An A-level Computer Science project by Samuel Newman
ArbitraIntroductionProject AimsLanguages and softwareNamesUsageAnalysisResearchA brief introduction to CryptocurrenciesMore on walletsObjectivesBasic ProtocolPrototyping Core FunctionsHashing and BlocksElliptic Curve Digital Signature AlgorithmDisclaimerElliptic CurvesPoint AdditionFinding the line between a and bFinding the third pointPython ImplementationPoint at InfinityPoint MultiplicationThe PatternThe SolutionFinite FieldsImplementing Modular Inversemod pSide note regarding notationGraphs and mod pThings we know about the point at infinityObjectifyingBack to CryptographyPicking a curveCurve CharacteristicsCreating a signatureVerifying a signatureVerifying the programConclusionNetworkingApplication planningConceptConcept 1Concept 2Concept 3PagesKeyDocumented DesignHow the network system worksA closer look at the ProtocolData typesHeaderMessage typesTransactionBlockPingNode RequestLatest Block Hash RequestChain RequestReply typesPing replyChainBlock hashNodeReceivedError MessageNetwork diagramsSimulation 1: From S1Simulation 2: From S1User Interface DesignConcept 1RedesignSource codeHTML - index.htmlCSS - style.cssListsCreating the Electron appRemoving the FrameChanging pagesHighlighting Menu LinksThe crypto ModuleNetworkingServer ExampleClient ExampleData FilesFile PlanningTechnical SolutionConverting Python CodeECDSAsha256()onCurve()invMod()addPoints()multiPoints()createKeys()signMsg()verifyMsg()TestingSimplifying FunctionsHashingFilestore()Removing arraysTestingget()getAll()storeAll()append()Message Sending/Receivingconnect()setInterval()Message Parsing/ProcessingparseMsg()Parsing Different Message Typespg()TestingRestructuring parseMsg()tx()transaction()bk()hr()nr()cr()Sending MessagesParsing repliesParsing different reply typescn()nd()bh()ok()er()Exporting functionsBlockchaingetBlock()BalancescalcBalances()getTopBlock()mainChain()getChain()checkBalance()addBlock()PagesTransactionsWalletsCreating WalletsTestingCreate TransactionView recentBlockchainMiningMulti-threading alternativesMining pageMining ScriptTestingViewingSettingsNetwork SettingsManual PingTarget ConnectionsAdvertiseClear connectionsApplication SettingsFinal TouchesMoving CSSHeight counterIcon and Splash ScreenOverview pagenpm start scriptTestingTest 1 - Application fresh startMethodExpected OutcomesTest ResultProofTest 2 - Creating a walletMethodExpected OutcomesTest ResultProofTest 3 - Pinging a clientMethodExpected OutcomesTest ResultProofTest 4 - Automatic reconnectingMethodExpected OutcomesTest ResultProofTest 5 - Connected to backupMethodExpected OutcomesTest ResultProofTest 6 - Mine a blockMethodExpected OutcomesTest ResultProofIssuesFixesTest 7 - Make a transactionMethodExpected OutcomeTest ResultProofTest 8 - Add a transaction to blockchainMethodExpected OutcomesTest ResultProofTest 9 - Save wallets.jsonMethodExpected OutcomesTest ResultProofTest 10 - Invalid transactionMethodExpected OutcomesTest ResultProofEvaluationInitial ObjectivesPersonal ThoughtsCodeappstaticpagesjspages
Arbitra is a cryptocurrency – a way of performing transactions that are guaranteed by the rules of mathematics rather than a central bank or some other 3rd party. A blockchain shared amongst a decentralized peer-to-peer network is used to verify transactions.
The initial plan is to use the Electron framework. This means that the frontend will be using HTML/CSS, and the backend will use Javascript with a bit of Node.js, the Javascript runtime that is usually used for web server backends. This is helpful because the nature of cryptocurrencies involves a lot of peer-to-peer communication, which Node should excel at.
The reason I chose Javascript and Electron is that I have a lot of experience with web development, but next to no experience in the GUI systems of any other of the languages I know. Given the time constraints, having to learn a new language could be risky because of how long it would take. Electron will allow me to work with HTML and CSS, which I am very comfortable with, and using Javascript has turned out to have other benefits. With access to the Node Package Manager (npm), I should have no problem finding a cryptography package or anything else I might need.
Something to consider is that Bitcoin and Litecoin were made in C++ and Ethereum was made in Golang. However, since I have no experience in either of those languages I thought it would be safer to stick with what I am comfortable with. If performance with Javascript turns out to be too much of an issue I could potentially write the performance-critical parts of the application in a lower-level language like C++ but still use Javascript for the UI/networking/other logic.
The cryptocurrency is called Arbitra. However, the unit of currency is an Arbitrary Unit, or au.
The reason it is called Arbitra is because I thought it would be funny to have the unit of currency be an arbitrary unit. It also happens that using the shortened term is the same as the chemical symbol for gold, which has an amusing juxtaposition between something worthless (an arbitrary unit) and something valuable (gold).
I really like Arbitra. I have over 100 arbitrary units!
That will cost you 50au.
The Bitcoin whitepaper (https://bitcoin.org/bitcoin.pdf) was used to understand much of how a cryptocurrency works. "Ever wonder how Bitcoin (and other cryptocurrencies) actually work?" (https://www.youtube.com/watch?v=bBC-nXj3Ng4) by 1Brown3Blue was also very helpful. "Building a desktop application with Electron" (https://medium.com/developers-writing/building-a-desktop-application-with-electron-204203eeb658) was useful as reference when building the Electron app, as well as the Electron Quick Start Guide (https://electron.atom.io/docs/tutorial/quick-start/).
A transaction, most simply, is a message that says "I want to send this person x arbitrary units". The idea of a cryptocurrency is that rather than having a physical unit that you hand over to someone, you instead have a list of all transactions ever made and determine your account balance from that. This means that everyone can be certain that no-one is forging money or faking transactions - everyone can see the list and validate it. It also removes trust out of the equation - you don't have to just hope that your bank is keeping your money safe.
To create a transaction, you must have a “wallet” that has some currency in, and use a public/private key pair to cryptographically “sign” your transaction. Public/private key pairs are a cryptographic function that allows a private key to create a message with which one can use the public key to mathematical verify that the message was created with it's paired private key. You would then submit your transaction to a node on the network. If your transaction is valid, the node will send it to all other nodes in the network who will add it to a “block” of transactions.
A block consists of all transactions submitted since the previous block, the timestamp, the hash of the block that came before, and a nonce (which I’ll get to). Having the hash of the previous block irrevocably links the block to the one that came before it, and since it is easy to check, it is impossible to change the previous block’s contents without changing all the blocks that have come after. It also ensures that they are ordered. These blocks thereby form a chain from the genesis block, hence “blockchain”.
xxxxxxxxxx81{2 id: "cad944434a29dcfcfb4080cec264396fd23d73c1708db39bd780e7e30ef9072f",3 sender: "4a57bd2226eb76cceddf0cfe0baa2a1391b4952db4610dc3762845cedffdff62",4 recipient: "aed1fe98cda4ba5a1681a19aa768f73b9d707c5621c7effdf2938e242080505e",5 amount: 50.0,6 timestamp: "1505052733",7 signature: "5773487d221545d26fd0f57fdb3a7d986bc479a850d7b0d762e8c7f4772790a0"8}This is an example of a transaction, represented in JavaScript Object Notation (JSON). A block would be a collection of these.
A blockchain is the equivalent of a bank's ledger - all transactions are recorded to keep track of everyone's balance. The difference between a ledger and a blockchain is that a ledger is kept secret by the banks, whereas the blockchain is held by anyone who wants it. This guarantees that the transactions cannot be messed with.
Anyone can download the blockchain and see if it is valid. But how do you stop someone adding a malicious yet technically valid block to the chain? This is what the nonce is for. In order to be added to the blockchain, the block is hashed using SHA-256 and if the hash meets some criteria, it is sent to all the other nodes in the network. Otherwise, the nonce is changed over and over until the right hash is found. This means that it takes a lot of work to find a valid block, and this work can be verified almost instantly.
This means that if someone wants to submit a malicious block, they will have to either get impossibly lucky, or have more computing power at their disposal than the rest of the network, which is infeasible. Furthermore, even if you do manage to validate a malicious block, nodes will always accept the longest chain, so you would have to keep adding to your malicious chain effectively forever, which gets exponentially harder.
This means, to become a node in the network, you (or your computer) would need to:
Hashing the block in order to find the correct nonce is called “mining”.
In order to incentivize mining, each block contains a transaction at the top that gives some amount of currency to whoever mines the block. This means that a miner can recoup the cost of the electricity and equipment spent mining the block. It also means that, if you had 51% of the CPU power of the network, it could well be more profitable to play by the rules and get the mining rewards rather than stealing coins.
Unlike in Bitcoin and most other cryptocurrencies, Arbitra will not reduce the mining rewards over time to cap the number of coins. This is to avoid the issue of requiring transaction fees, as this penalizes poorer users. It does mean that Arbitra will inflate over time, but unlike fiat currencies it will be completely predictable.
Something else to consider is that once a block is added it does not mean that it is there to stay. Even if a block is mined, if another block is also found at the same time (a "branch") it depends on which one ends up having the longer chain follow it. Having multiple competiting blocks is intentional, to ensure legitimacy, but this also means that the "top layer" of the blockchain is not necessarily trustworthy.
However, since it becomes exponentially harder to keep up with the main chain if a branch starts lagging behind. This should ensure that smaller branches die off quickly
A client application, which is what most ordinary people would use, does not do any mining. It simply checks the blockchain to see how much money is attributed to it, and it can sign and send transactions to the network. In this project I will make both a client and a node, but wrapped into one application for simplicity's sake.
As you can see, there is no need to trust any one central authority in the network once it starts. For as long as the cryptographic principals hold, the currency is effectively a democracy based on computing power - the blockchain is controlled by those who hold the majority of computing power, and this will almost certainly never be controlled by one party.
Something that must be cleared up - a wallet is not like a bank account. All that is needed for a wallet is a private key, which can be created freely. You also don't need to register your wallet with any central authority, once again ensuring the honesty and security of the network. All a wallet is is a random number. It also solves the problem of everyone seeing every transaction - it doesn't matter because your balance is split up amongst a bunch of anonymous keys.
These objectives estimate what must happen for the project must be a success.
From the description of the cryptocurrency, we can determine the format of the most important types messages that would be sent in the Arbitra network. Most simply, the basic message types and their contents are:
Transaction
-- Transaction ID
-- Recipient
-- Amount
-- Signature
-- Timestamp
Block
-- Previous block’s hash
-- Timestamp
-- Nonce
-- Number of transactions
-- List of transactions
-- It's own hash (added after the nonce is found)
Recognise Request
This is effectively a ping asking a node to add them to it's list of known nodes
Latest Block Request
Node will send back all the blocks at the top of the chain
Block Request -- Hash of requested block Node will send back a block with the requested Hash
Error -- Type of error This is what a client replies with if it receives an invalid message
When a new node joins the network, it will send a Latest Block Request to a few nodes, and will use the previous block’s hash to fill in the chain from the top. If the Latest Block Request returns a few different blocks, the system should default to the longest chain, and only use the other blocks if no other nodes recognize that block. The specific details on how that process should work needs investigation.
In order to working out the specifics of how this is going to work, I decided to use Python to implement some of the functions that a real client would use. The final client will be an Electron app made with Node.js, but due to the ease of iteration in Python I think that using Python for testing purposes would be easier and simpler.
First off, I needed to make a simple hashing function. I created a wrapper around the SHA256 function from the hashlib library so that I could easily get the SHA256 hash of a string.
xxxxxxxxxx61import hashlib23def sha256(inputstr):4 h = hashlib.sha256()5 h.update(inputstr.encode("utf-8"))6 return h.hexdigest()First, I import hashlib at the top of the program.
Then, I define a function called sha256(), which takes in a variable called inputstr.
sha256() creates a hashlib.sha256() object, which we can use to create a hash.
We then update() the hashlib object with the string we want to hash. Notice we encode inputstr, which is because hashlib can only hash bytes, not characters, so we call encode("utf-8") on it.
Finally, we return the hexdigest() of the object. This converts the object into a hex string.
This produces the following result:
xxxxxxxxxx21>>> sha256("something")2'3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb'
It worked. We now can hash any string. Therefore, the next step is to prototype mining. The next program should then, while the output does not fit a criteria, repeatedly hash a random string with a nonce. To fit the criteria, the string must begin with a certain number of zeros.
xxxxxxxxxx181def mine(inputstr):2 DIFFICULTY = 43 nonce = 04 fail = False5 while(True):6 hashed = sha256(str(inputstr)+str(nonce))7 for i in range(DIFFICULTY):8 if hashed[i] != "0":9 fail = True10 if not fail:11 print("INPUT:", inputstr)12 print("NONCE:", nonce)13 print("HASH:", hashed)14 break15 else:16 print(nonce)17 fail = False18 nonce += 1mine(), like the other function, takes an input called inputstr.
It then sets up the constant DIFFICULTY and the initial variables, nonce and fail.
DIFFICULTY is the number of zeros that need to be at the beginning of the hash for it to pass.
nonce is (obviously) the nonce. This iterates each time the hash does not pass.
fail is a boolean that is false, and will set to true if any one of the first 4 (or whatever the difficulty is) characters of the hash is not zero.
There is then a while loop that will loop forever until it is broken.
Then, inputstr and nonce are hashed using the sha256 function we made earlier, and the hash is assigned to hashed
When the hash is found, a for loop is used to see if the any of the first few characters of the hash are not zero. If so, fail is set to True so that when the loop is over it fails the test if any one of the first few characters are zero.
If the test did not fail, it prints out the input, the final nonce and the final hash, then breaks the while loop, ending the function. If it were a proper function, it would return these values instead of printing them
If it did fail, it iterates the nonce and resets fail. It also prints the nonce so that you can see the progress.
xxxxxxxxxx111>>> mine("something")2031425...6543927543938543949INPUT: something10NONCE: 5439511HASH: 00001d711101fe4555b9e644cbf85ad205db46d052d4b7af4f28b80d9476c391
It found a hash that begins with 4 zeros in only 54395 iterations. This took quite a long time (9 minutes 30 seconds). This is way too long - the target time is (currently) 5 minutes. However, this was only one laptop. With a network of computers around the world checking random nonces and a different difficulty, this can be achieved. In fact, as the overall computing power of the network increases, the difficulty will need to increase with it. To confirm that it works, I changed the difficulty to 2 and made it so that it also prints the hash as well as the nonce when it fails.
xxxxxxxxxx121>>> mine("something")2cad944434a29dcfcfb4080cec264396fd23d73c1708db39bd780e7e30ef9072f 03aed1fe98cda4ba5a1681a19aa768f73b9d707c5621c7effdf2938e242080505e 145773487d221545d26fd0f57fdb3a7d986bc479a850d7b0d762e8c7f4772790a0 25...64925d66314281301618fffc8d1b262f94910aab08c748710a7d736abbe266799 34274a57bd2226eb76cceddf0cfe0baa2a1391b4952db4610dc3762845cedffdff62 343835bc236517ea9d427e0fcd912e267b4fb11d7b1364b00224b632944b5852236a 34495e5ea76515cac322cb94794eec77f9ef1969df95e4c1dcb7dd55d7fa0b3db0d3 34510INPUT: something11NONCE: 34612HASH: 0083dde42af5cb39d72decae7004c18d11b34969faa70d2d5a6ce3e167a6edf9
As you can see, with the difficulty reduced to 2 it only took 346 iterations, which took about 5 seconds. In this way, we can dynamically set the difficulty so that as the computing power of the network increases we can make sure that the time to mine each block stays about the same. If we want more granular control of the difficulty, we could make it so that it requires, for example, 2 zeros and 3 numbers less than 5.
At this early stage, statements like these are guesswork, but I think that there will be a target time (5 minutes) and if a block is significantly earlier than that the target time the number of zeros increases by one and vice versa if it takes too long.
To sign messages in Arbitra, we are going to use the Elliptic Curve Digital Signature Algorithm, or ECDSA. As previously explained, Digital Signatures are a way of verifying that a message was sent from someone, by including a private key that can only have been generated by the private key, which only the sender should know. We can use a mathematical concept called Elliptic Curves to sign the messages in Arbitra.
Rather than using the Node.js crypto module's implementation, I decided to implement it myself, so I understand what's happening behind the scenes. I found a paper called Implementation of Elliptic Curve Digital Signature Algorithm, which provides some insight into the mathematics behind ECDSA, as well as an article called Understanding How ECDSA Protects Your Data. The best article I found was called Elliptic Curve Cryptography: a gentle introduction, as it covers the mathematics in depth enough to implement, but not assuming prior knowledge as the majority of other articles did, as well as providing Python examples of some algorithms.
https://pdfs.semanticscholar.org/c06a/d6512775be1076e4abd43e3f2928729da776.pdf
http://www.instructables.com/id/Understanding-how-ECDSA-protects-your-data/
http://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/
The reason I decided to implement ECDSA myself is that I could not figure out how to do it in the crypto module, and when I realised how to do it by that point I'd already done way too much research to back down. Still, it has proved a valuable learning experience.
Elliptic curves are curves of the form .
But how does this relate to cryptography?
From http://wstein.org/edu/2007/spring/ent/ent-html/node89.html:
Suppose is an elliptic curve over and . Given a multiple of , the elliptic curve discrete logarithm problem is to find such that .
This means that it's really hard to find when you only have and - so hard, that we can use this problem to prove mathematically that has been generated by . This is how we sign messages in Arbitra - it shows that the message has to have been signed by the person who has the secret key.
However, before we start that we need to understand the operations we can perform on points on the curve, which is different to normal arithmetic. There are some operations we can perform:
Point addition is the process of adding two points together to find a third. This is best shown by this interactive Desmos graph: https://www.desmos.com/calculator/ialhd71we3

To add two points, you draw a straight line between them, and then find the negative of the third root when this line meets the curve.
This is very simple to calculate graphically, and relatively simple to calculate with pen and paper. However, to create a function we will need to create a formula using and .
It would be easier to just have to input and and for the function to calculate the corresponding values. Using the curve , we can find them using:
To find the equation of the line between the points, we need to find the gradient of the line, which is given by:
The equation of the line is of the form , and since we have values for , , and , we can rearrange to find :
This is the -intercept of the straight line.
is given by:
We need to find . We can use:
The point we are trying to find is therefore given by:
I wrote this into a Python function, where the curve is
xxxxxxxxxx131import math23def addPoints(x1, x2):4 # adding two points on an elliptic points5 y1 = math.sqrt(x1**3 + 7)6 y2 = math.sqrt(x2**3 + 7)7 # m is the gradient of the line between the points8 m = (y1-y2)/(x1-x2)9 # c is the y intercept10 c = y1 - m*x111 x3 = m**2 - x1 - x212 y3 = m*x3 + c13 return x3, -y3From the interactive Desmos graph, inputting values of 1 and 3 gave .
xxxxxxxxxx21>>> addPoints(1,3)2(-1.7462112512353212, 1.2943565281332712)
The function works. It doesn't handle edge cases yet, but we'll cover that later.
The second operation we can perform is point doubling. From this graph:

It looks as if point is the negative of the second intercept of the tangent from point . This makes sense, as , and doing point edition with the same point will result in the tangent of that point. Unfortunately, since we find the gradient of the line using , if that would divide by zero. Therefore we need to differentiate using the chain rule:
We can now implement this in Python - I just used the same addPoints() function for simplicity. Also more comments.
xxxxxxxxxx171import math23def addPoints(x1, x2):4 # for the line y^2 = x^3 + 75 y1 = math.sqrt(x1**3 + 7)6 y2 = math.sqrt(x2**3 + 7)7 # m is the gradient of the line between them8 if x1 == x2: # if points are the same, find the tangent9 m = (3*(x1**2))/(2*math.sqrt((x1**3)+7))10 else: # otherwise find gradient normally11 m = (y1-y2)/(x1-x2)12 # y-intercept13 c = y1 - m*x114 # finding 3rd point15 x3 = m**2 - x1 - x216 y3 = m*x3 + c17 return x3, -y3xxxxxxxxxx21>>> addPoints(1,1)2(-1.71875, -1.386592203732996)
We can verify this result using this tool, from Elliptic Curve Cryptography: a gentle introduction, since it takes this into account while the other tool doesn't.
https://cdn.rawgit.com/andreacorbellini/ecc/920b29a/interactive/reals-add.html?px=1&py=2&qx=1&qy=2
Therefore this function works. Before we finish, I wanted to make sure we could easily change the curve, by changing it to . This means that we have to recalculate some of the maths, most notably the tangent equation:
I also noticed we could simply the tangent equation given we know already. All the changes that were necessary to the python function were changing the differential and changing how it finds y1 and y2.
xxxxxxxxxx191import math23def addPoints(x1, x2):4 # for the line y^2 = x^3 + 75 a = 06 b = 77 y1 = math.sqrt(x1**3 + a*x1 + b)8 y2 = math.sqrt(x2**3 + a*x2 + b)9 # m is the gradient of the line between them10 if x1 == x2: # if points are the same, find the tangent11 m = (3*(x1**2)+a)/(2*y1)12 else: # otherwise find gradient normally13 m = (y1-y2)/(x1-x2)14 # y-intercept15 c = y1 - m*x116 # finding 3rd point17 x3 = m**2 - x1 - x218 y3 = m*x3 + c19 return x3, -y3When a = 0 and b = 7 it gives the same results:
xxxxxxxxxx41>>> addPoints(1,3)2(-1.7462112512353212, 1.2943565281332712)3>>> addPoints(1,1)4(-1.71875, -1.386592203732996)
With a = -7 and b = 10, it gives:
xxxxxxxxxx41>>> addPoints(1,3)2(-3.0, 2.0)3>>> addPoints(1,1)4(-1.0, -4.0)
Which can be verified with the tool:
https://cdn.rawgit.com/andreacorbellini/ecc/920b29a/interactive/reals-add.html?px=1&py=2&qx=1&qy=2
What happens if the line doesn't intersect with the curve
If the line does not intersect, we say that it intersects the point at infinity, . This only happens if you try to double where . However, this is not critically important at this phase, although we will have to add this in as an exception when creating the real function later on, as it will break the function (which is not good).
Now that we can do the basic function, we need to be able to multiply points, as we need to get to the stage where we can calculate . The obvious way would be to double , and then calculate for number of times. However, as Elliptic Curve Cryptography: a gentle introduction points out, this equation has an efficiency of , which is not particularly fast. However, they suggest using the double and add method to multiply points.
In summary, if you repeatedly double , you would get the pattern:
And we can can represent a decimal number in the following form:
Therefore:
Since we can find using point doubling, and since we can represent a large number very easily using binary, we can massively cut down on the calculations needed to multiply a point. In our example, five doublings and one addition is needed to find and in the article, they show that only seven doublings and four additions are needed to find . This brings the complexity down to , which is much faster for larger numbers.

We can implement this in python (note that this is without looking at the given example code):
xxxxxxxxxx131def multiPoints(n,P):2 a = 03 b = 74 # find binary equivlent of n5 # and take the first 3 digits off6 nb = str(bin(n))[3:]7 total = P8 # and reverse it9 for bit in nb[::-1]:10 P,y = addPoints(P,P)11 if bit == "1":12 total,y = addPoints(total,P)13 return total, math.sqrt(total**3 + a*total + b)The reason that it takes the first three digits off of the binary string is that they firstly are not of fixed length, so always start with 1, and secondly begin with 0b. Knowing this, we can remove the first 3 digits and set total to start with the value .
This gives:
xxxxxxxxxx21>>> multiPoints(5,5)2(-1.562727156221965, 1.7842754133340577)
We can verify this with this calculator:
https://cdn.rawgit.com/andreacorbellini/ecc/920b29a/interactive/reals-mul.html
Unfortunately it does not work, and it turns out the reason is that I made a logical error in the code. Even though the first digit of the binary sequence is always , because that is the most significant bit it is actually the last number we want. The least significant bit, , can be or , and so the corrected code looks like this (with total now initialising to and the loop rearranged):
xxxxxxxxxx171def multiPoints(n,P):2 a = 03 b = 74 # find binary equivlent of n5 # and take the first 2 and last digits off6 # first 2 are "0b" and last is definitly a 17 nb = str(bin(n))[2:]8 total = 09 # and reverse it10 for bit in nb[::-1]:11 if bit == "1":12 if total:13 total,y = addPoints(total,P)14 else:15 total = P16 P,y = addPoints(P,P)17 return total, math.sqrt(total**3 + a*total + b)Strangely enough, this doesn't work for some numbers, but still does for others - looping though 0 to 24 produces the correct answers for 1, 2, 3, 4, 8, 9, 10, 11, 16, 17, 18, 19, and 24 (with the caveat that the y value is always positive because of the square root), but incorrect answers for the rest. Why is that? For 0 it is because there is starts at zero when it should start at infinity - that will be fixed. But the rest are strange, especially considering that it starts getting numbers incorrect, but then starts being correct again, which should not be possible considering each number relies on the previous one.
On top of this, it forms a sequence of 4 correct, 3 wrong, 4 correct, 4 wrong, which is very odd.
I decided to map out 1 to 10, with their binary equivalent, the answer the function gave, and the answer the calculator gave.
| Decimal | Binary | Function | Calculator | Correct? |
|---|---|---|---|---|
| 1 | 1 | 5 | 5 | Yes |
| 2 | 10 | 0.6534090909090917 | 0.6534 | Yes |
| 3 | 11 | -1.562727156221965 | -1.56275 | Yes |
| 4 | 100 | -1.250473444973121 | -1.25046 | Yes |
| 5 | 101 | -1.5627271562219653 | 1.07934 | No |
| 6 | 110 | 0.6534090909090918 | 7.3407 | No |
| 7 | 111 | 5.000000000000002 | 169.37407 | No |
| 8 | 1000 | 3.5915053505509373 | 3.59139 | Yes |
| 9 | 1001 | 0.24372294383777238 | 0.24367 | Yes |
| 10 | 1010 | -1.7888421516340784 | -1.78888 | Yes |
Some observations:
I noticed that when it started to go wrong, it was after the previous x value was negative, which I confirmed using the calculator. What could cause this? Of course, the usual answer is something to do with square roots. If we look at the addPoints() function:
xxxxxxxxxx171def addPoints(x1, x2):2 # for the line y^2 = x^3 + 73 a = 04 b = 75 y1 = math.sqrt(x1**3 + a*x1 + b)6 y2 = math.sqrt(x2**3 + a*x2 + b)7 # m is the gradient of the line between them8 if x1 == x2: # if points are the same, find the tangent9 m = (3*(x1**2)+a)/(2*y1)10 else: # otherwise find gradient normally11 m = (y1-y2)/(x1-x2)12 # y-intercept13 c = y1 - m*x114 # finding 3rd point15 x3 = m**2 - x1 - x216 y3 = m*x3 + c17 return x3, -y3There it is - calculating y1 and y2 uses math.sqrt(). This effectively means that it only adds points above the line . We need to work around the square root in order for the algorithm to work in all cases.
The solution is probably to not calculate the y values in this function. Instead both the x value and the y value should be passed to the function. The best way to do this is to group points into a tuple like so: point = (4,6). Then when we want to get those variables back:
xxxxxxxxxx21x, y = point2# x is 4, y is 6Alternatively we could use a list or even an object, but this is probably the simplest way.
Since I will soon be rewriting this function, I won't make changes here. As shown previously, the algorithm gets back on course after encountering a negative x value, so the multiPoint() function itself seems to work pretty well. All that is left to do is to add exceptions for multiplying by , which I will do later on.
At this point, we can now find . However, notice that we have no "point subtraction" or "point division". What if we wanted to find (the private key) from and ? That is the logarithm problem, and the point of doing all this is to make that as hard as possible - the harder it is, the more secure the algorithm is. The next step is to make it even more difficult to find .
The way that ECDSA makes it harder is by using finite fields, which limits the number of elements we have. This makes the curve into a finite number of discrete points, and should make it much harder to find . The problem, as stated at the beginning of the ECDSA section, is the discrete logarithm problem.
To restrict to a finite field, we use modular arithmetic.
In summary, the modulus operation finds the remainder when dividing one number by another. There are five different operations we can do:
The last one is called the modular inverse, and is the most important. It is the equivalent of a modular division, because multiplying by an inverse number is the same as dividing by a non-inverse number.
Modular addition, subtraction, multiplication and exponentiation can all be calculated using Python's modulus operator %. We can also find the quotient without the remainder using //.
xxxxxxxxxx61>>> 20/722.8571428571428573>>> 20//7425>>> 20%766
However, there is no built-in function to find the inverse mod of a number, so we need to implement our own algorithm to find it. The naïve approach would be to iterate through possible values of (also known as ).
xxxxxxxxxx51def slow_inv_mod(n,p):2 for i in range(p):3 if (n*i)%p == 1:4 return i5 raise ValueError(str(p)+" is not prime")Unfortunately that runs slowly at . We need a more efficient algorithm, and for that we need to implement the Extended Euclidean Algorithm, which has a complexity of . It takes in two numbers and , and returns and that satisfy the equation:
Where is the greatest common divisor of the two numbers.
We can calculate the easily using recursion:
xxxxxxxxxx51def gcd(a, b):2 if a and b:3 return gcd(b,a%b)4 else:5 return a or bFor example:
xxxxxxxxxx21>>> gcd(270,192)26
However, the algorithm itself works differently.
Implementing the pseudocode found on Wikipedia:
https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Pseudocode
xxxxxxxxxx141def eec(a,b):2 x, old_x = 0, 13 y, old_y = 1, 04 r, old_r = b, a56 while r != 0:7 quot = old_r // r8 old_r, r = r, old_r - quot * r9 old_x, x = x, old_x - quot * x10 old_y, y = y, old_y - quot * y11 12 # ax + by = gcd(a,b)13 # returns (gcd, x, y)14 return old_r, old_x, old_ySince Wikipedia was kind enough to provide a trace table of the algorithm, we can confirm that it works. For the input , , we expect the values , , .
xxxxxxxxxx21>>> eec(270,192)2(2, -9, 47)
We can further confirm that the is 2 using the gcd() function.
xxxxxxxxxx21>>> gcd(270,192)22
Now for the actual inverse modulus function. Something to mention at this point is that must be a prime number. This is because must be equal to so that it can be used in the modular inverse function - in fact, only has an inverse if .
xxxxxxxxxx61def invMod(n,p):2 gcd, x, y = eec(n,p)3 if gcd == 1:4 return x%p5 else:6 raise ValueError(str(p)+" isn't prime (or n = 0)")Whilst this seems too simple to be correct, as far as I can tell that is all that is needed, since if is the inverse of :
and we know that this equation is equal to if is prime:
therefore if we find the of all values:
and since and :
This is the same form as the modular inverse. Now all we need to do is find so that we can then later use it in the form (or similar).
Now we have this algorithm down, it is time to explore the implications of .
If you were to find the modulus of all real numbers - - you would be splitting up into groups.
Here is :

Image from Khan Academy: https://www.khanacademy.org/computing/computer-science/cryptography/modarithmetic/a/congruence-modulo
When we do that, it is known as a Finite Field, and is represented as . It contains all integers from to . In the above example, that is the finite field , and contains , , , , . Whilst this doesn't seem too useful, we can still do maths on it - except instead of normal arithmetic, we use the modular arithmetic. For example, if we wanted to do in , we would have to do , which is .
Whilst previously I had been using , I will now be switching to , because it indicates that it's all modulus , which will be the case from now on.
The most important thing about curves in is that instead of being a continuous line, they become a bunch of discrete points. The bigger that is, the more points there are. The axis also only goes from to (therefore "finite field").
Something else important to understand about modulus is how it "wraps around". If we were to find , it doesn't matter that as it just removes over and over until the resulting number is less than , which is the remainder. That is why it is know as "clock arithmetic", because if you wanted to find the time 50 hours after 2pm, you would go around the clock until the resulting number is less than 24. We can therefore represent this problem as , which is , and therefore 4am.
This means that a plane in the axis in the finite field would only go from to , but when a line in the graph reaches the edge of the plane it wraps around to the other side.
This means that we can still add and multiply points, as visually demonstrated in this calculator:
https://cdn.rawgit.com/andreacorbellini/ecc/920b29a/interactive/modk-add.html
However, we now need to update our point adding function. Instead of adding points on the curve , it now needs add points on the curve . This basically includes adding %p to the end of all our functions, but we need to make some deeper changes (for example to account for the point at infinity)
Before we start, two things:
secp256k1 curve, for example, .can just be an argument passed to the function in this case.
Since we will need to remake most of the functions that we've made so far, I decided to take this opportunity to clean up the code a bit. To start, I made a elliptic_curve class so that we can define , and once, and then use ec.p (for example) if we wanted .
xxxxxxxxxx71class elliptic_curve():2 def __init__(self,a,b,p):3 self.a = a4 self.b = b5 self.p = p67ec = elliptic_curve(0,7,97)In this example, we have made the curve .
Next, we need to think about how the addPoints() function works. First of all, we need to take into account the changes mentioned in the point multiplication section - passing both co-ordinates of the points.
xxxxxxxxxx31def addPoints(P1,P2):2 x1,y1 = P13 x2,y2 = P2Second of all, we need to make sure that the points are actually on the curve - previously, we dealt with that by only requiring one value. Now, we have to make sure the inputs make sense. To do this, we will make a new function that makes sure that by raising a ValueError if it doesn't:
xxxxxxxxxx41def onCurve(point):2 x,y = point3 if (y**2 - x**3 - ec.a*x - ec.b) % ec.p != 0:4 raise ValueError("({},{}) is not on the curve".format(x,y))Now addPoints() looks like this:
xxxxxxxxxx71def addPoints(P1,P2):2 # make sure they're on the curve3 onCurve(P1)4 onCurve(P2)5 6 x1,y1 = P17 x2,y2 = P2Now we need to account for the . This is simple enough.
xxxxxxxxxx51 ... 2 # need to define m3 4 x3 = (m**2 - x1 - x2) % ec.p5 y3 = (y1 + m*x3 - m*x1) % ec.pHowever, we need to find the gradient, and there we have a problem.
Both these equations have a division in them, and we can't do that!
We need to rearrange them like so:
Now we can use our inverse modulus algorithm.
xxxxxxxxxx101def addPoints(P1,P2):2 ...3 # finding gradient4 if x1 == x2:5 m = ((3*(x1**2)+3)*invMod(2*y1,ec.p))6 else:7 m = ((y2 - y1)*invMod(x2-x1,ec.p))8 9 x3 = (m**2 - x1 - x2)10 y3 = (y1 + m*x3 - m*x1)Finally, we can the resultant point.
xxxxxxxxxx191def addPoints(P1,P2):2 # make sure they're on the curve3 onCurve(P1)4 onCurve(P2)5 6 x1,y1 = P17 x2,y2 = P28 # finding gradient9 if x1 == x2:10 m = ((3*(x1**2)+3)*invMod(2*y1,ec.p))11 else:12 m = ((y2 - y1)*invMod(x2-x1,ec.p))13 14 x3 = (m**2 - x1 - x2) % ec.p15 y3 = -(y1 + m*x3 - m*x1) % ec.p16 P3 = (x3,y3)17 onCurve(P3)18 19 return P3This should work in most cases, but we're missing something - the point at infinity.
The most important point here is - that means we can just return P1 if P2 == O, and vice versa. It also means that if x1 == x2 but y1 != y2, the third point is .
The thing to notice in all these points is that none of these require to be an actual number, as if it is passed to the function, it returns something before the maths starts. Therefore, I have decided for to be the string "infinity", so it definitely cannot be confused, and will throw a TypeError if I make a mistake and it manages to get through to the maths.
With this in mind, I created the final function:
xxxxxxxxxx281def addPoints(P1,P2):2 # make sure they're on the curve3 onCurve(P1)4 onCurve(P2)56 if P1 == "infinity":7 return P28 elif P2 == "infinity":9 return P11011 x1,y1 = P112 x2,y2 = P21314 # finding gradient15 if x1 == x2:16 if y1 != y2:17 return "infinity"18 else:19 m = ((3*(x1**2)+ec.a)*invMod(2*y1,ec.p))20 else:21 m = ((y2 - y1)*invMod(x2-x1,ec.p))2223 x3 = (m**2 - x1 - x2) % ec.p24 y3 = -(y1 + m*x3 - m*x1) % ec.p25 P3 = (x3,y3)26 onCurve(P3)2728 return P3I tested it out with the points and , which I knew were on the curve thanks to the calculator:
xxxxxxxxxx21>>> addPoints((5,36),(20,21))2(73, 32)
We can verify it with this calculator:
https://cdn.rawgit.com/andreacorbellini/ecc/920b29a/interactive/modk-add.html

It worked. It should now be simple to convert the point multiplication function.
xxxxxxxxxx151def multiPoints(n,P):2 onCurve(P)3 if P == "infinity":4 return P5 # find binary equivlent of n6 # and take the first digits off7 nb = str(bin(n))[2:]8 total = "infinity"9 # and reverse it10 for bit in nb[::-1]:11 if bit == "1":12 total = addPoints(total,P)13 P = addPoints(P,P)14 onCurve(P)15 return totalmultiPoints() is now much simpler, because of two things:
total as which will give the correct answer, rather than manually setting total to the correct answer after the first run.Now for testing:
xxxxxxxxxx21>>> multiPoints(20,(20,21))2(62, 54)
Which we can verify with the calculator:
https://cdn.rawgit.com/andreacorbellini/ecc/920b29a/interactive/modk-mul.html
It worked.This is all of the functions that we need to actually create signatures.
I decided to move all the functions into one class (elliptic_curve()), to make it simpler.
Here is the class in it's entirety:
xxxxxxxxxx781class elliptic_curve():2 def __init__(self,a,b,p):3 self.a = a4 self.b = b5 self.p = p6 7 def onCurve(self,point):8 if point != "infinity":9 x,y = point10 if (y**2 - x**3 - self.a*x - self.b) % self.p != 0:11 raise ValueError("({},{}) is not on the curve".format(x,y))12 13 def eec(self,a):14 x, old_x = 0, 115 y, old_y = 1, 016 r, old_r = self.p, a1718 while r != 0:19 quot = old_r // r20 old_r, r = r, old_r - quot * r21 old_x, x = x, old_x - quot * x22 old_y, y = y, old_y - quot * y23 24 # ax + by = gcd(a,b)25 # returns (gcd, x, y)26 return old_r, old_x, old_y2728 def invMod(self,n):29 gcd, x, y = self.eec(n)30 if gcd == 1:31 return x % self.p32 else:33 raise ValueError(str(p)+" isn't prime (or n = 0)")3435 def addPoints(self,P1,P2):36 # make sure they're on the curve37 self.onCurve(P1)38 self.onCurve(P2)3940 if P1 == "infinity":41 return P242 elif P2 == "infinity":43 return P14445 x1,y1 = P146 x2,y2 = P24748 # finding gradient49 if x1 == x2:50 if y1 != y2:51 return "infinity"52 else:53 m = ((3*(x1**2)+self.a)*self.invMod(2*y1))54 else:55 m = ((y2 - y1)*self.invMod(x2-x1))5657 x3 = (m**2 - x1 - x2) % self.p58 y3 = -(y1 + m*x3 - m*x1) % self.p59 P3 = (x3,y3)60 self.onCurve(P3)61 return P36263 def multiPoints(self,n,P):64 self.onCurve(P)65 if P == "infinity":66 return P67 # find binary equivlent of n68 # and take the first digits off69 nb = str(bin(n))[2:]70 total = "infinity"71 # and reverse it72 count = 073 for bit in nb[::-1]:74 if bit == "1":75 total = self.addPoints(total,P)76 P = self.addPoints(P,P)77 self.onCurve(P)78 return totalWe don't even need to import math anymore.
All we need to do to interact with this class is create a curve:
xxxxxxxxxx11ec = elliptic_curve(0,7,97)Then call the functions from that object:
xxxxxxxxxx11print(ec.multiPoints(5,(20,21)))Now that we have all the functions we need, we can soon start actually creating and verifying signatures!
However first, we need some finally things.
I found the very useful website http://safecurves.cr.yp.to that has a list of many of the elliptic curves that have been found. It also rates their security based on several different factors. Interestingly secp256k1, the curve Bitcoin uses, is not rated as safe.
However, before we pick a curve, there is something that needs to be mentioned about curves - there are different kinds. All the work so far has been for Weierstrass curves - curves with the equation that satisfy the equation .
Elliptic curves can take other forms, such as the Edwards curves of the form
Since using other curve forms would require redoing a lot of our maths, and converting from one curve type to another is very complex (I tried), I decided just to use a Weierstrass curve. Although none were deemed safe by http://safecurves.cr.yp.to/, I decided on secp256k1, which is defined as:
I chose secp256k1 for several reasons:
secp256k1 than other curves, for example I was not able to find the subgroup order when trying to implement a different curveThe characteristics of the curve are specified in this document, from the Standards of Efficient Cryptography group:
http://www.secg.org/sec2-v2.pdf
We don't just need the the and values for the curve, we also need some other values. First of all, we need the Base Point . There is a specific value for each curve, and it's just a static number you can look up. So for secp256k1 the base point is:
But what is the base point? A base point generates a subgroup - a subsection of the points on the curve. If you find the multiples of a point, it generates a group of coordinates that eventually loops back on itself.
The order of a group is the number of points in it. The number of points in the curve as a whole is it's order, .
Therefore the subgroup order is the number of points in the subgroup created by our base point. For secp256k1:
Finally there is the subgroup cofactor, which is just . I don't believe this will come up in the calculations.
We can add these characteristics to our Python class:
xxxxxxxxxx11ec = elliptic_curve(0,7,115792089237316195423570985008687907853269984665640564039457584007908834671663,(55066263022277343669578718895168534326250603453777594175500187360389116729240,32670510020758816978083085130507043184471273380659243275938904335757337482424),115792089237316195423570985008687907852837564279074904382605163141518161494337)Finally, we have all the functions that we need to start signing messages.
First off, what we're signing needs to be the same bit length as , the subgroup order. Since we hash the messages using sha256, and we're using the curve secp256k1, they both have the same big length of, unsurprisingly, 256. The message we're signing is denoted as .
We also need a public key and a private key. The private key is a random integer chosen from , and the public key using the scalar multiplication function. I created a Python function to create these:
xxxxxxxxxx41def createKeys(self):2 private = randrange(1,self.n)3 public = self.multiPoints(private,self.g)4 return (private,public)randrange() is from the random module, which I imported at the top of the program using from random import randint.
To create a signature, we need to follow these instructions:
From http://andrea.corbellini.name/2015/05/30/elliptic-curve-cryptography-ecdh-and-ecdsa/
The signature is then .
Implementing that in Python, as a part of the elliptic_curve() class:
xxxxxxxxxx111def signMsg(self,msg,w):2 z = sha256(msg)3 while True:4 k = randrange(1,self.n)5 P = self.multiPoints(k,self.g)6 xP,yP = P7 r = xP % self.n8 if r != 0:9 s = (self.invMod(k)*(z + r*w) ) % self.n10 if s != 0:11 return (r,s)We also can now verify a signature.
From the same source, the method to verify a secret key is:
If , then the signature is valid.
We can implement this in Python as well:
xxxxxxxxxx71def verifyMsg(self,msg,signature,q):2 r,s = signature3 z = sha256(msg)4 u1 = (self.invMod(s)*z) % self.n5 u2 = (self.invMod(s)*z) % self.n6 x,y = self.addPoints(self.multiPoints(u1,self.g),self.multiPoints(u2,q))7 return r == x % self.nWe need to test the program. At first, we will sign the string "hello", and then verify it.
xxxxxxxxxx51public,private = ec.createKeys()23signature = ec.signMsg("hello",private)45print(ec.verifyMsg("hello",signature,public))Unfortunately, this doesn't work.
xxxxxxxxxx61Traceback (most recent call last):2File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 121, in <module>3signature = ec.signMsg("hello",private)4File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 103, in signMsg5s = (self.invMod(k)*(z + r*w))6OverflowError: cannot fit 'int' into an index-sized integer
It's a strange error that I've never had before.
It then occurred to me that this is the first time that z is operated on, and z is , the hash. The hash, if you remember the function, returns the hash as a hex string. Presumably, this is what the error is referring to. I replaced the decimal inputs with hexadecimal inputs:
xxxxxxxxxx81# secp256k12ec = elliptic_curve(3 0,4 7,5 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,6 (0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,7 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8),8 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141)It still didn't work. I looked up the error, and apparently it's an issue converting Python ints to a C integer. Since the change makes the code more readable, I decided to keep it regardless.
I split up the offending line to see which operation causes the error.
xxxxxxxxxx31one = (z + r*w)2two = self.invMod(k)3s = (one*two) % self.nxxxxxxxxxx61Traceback (most recent call last):2File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 128, in <module>3signature = ec.signMsg("hello",private)4File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 103, in signMsg5one = (z + r*w)6OverflowError: cannot fit 'int' into an index-sized integer
Even more:
xxxxxxxxxx21rw = r*w2one = z + rwxxxxxxxxxx61Traceback (most recent call last):2File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 129, in <module>3signature = ec.signMsg("hello",private)4File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 103, in signMsg5rw = r*w6OverflowError: cannot fit 'int' into an index-sized integer
So clearly, r * w is causing an overflow error. I printed both values, and it turns out the w is (105339730627913794384744097333103439091751005834042565437622273746527120460296, 57526216366140097205440521137091723264218019353485851044212924740410855711206) - a point. This is probably the cause of the error. I looked at the createKeys() function, and it returns (private,public). However, when we call it:
xxxxxxxxxx11public,private = ec.createKeys()They're the wrong way round.We were passing the public key (a point) as the private key. Once I fixed it:
xxxxxxxxxx61Traceback (most recent call last):2File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 129, in <module>3signature = ec.signMsg("hello",private)4File "c:\Users\Mozzi\Documents\Programming\arbitra\ec.py", line 104, in signMsg5one = z + rw6TypeError: Can't convert 'int' object to str implicitly
Now we have a new error, and it was the error that I thought caused the other one. z is the wrong type - we need it to be an integer. At the moment, it's just a string. I replaced all instances of z = sha256(msg) with:
xxxxxxxxxx11z = int(sha256(msg),16)This should work:
xxxxxxxxxx11False
Whilst it's not throwing errors anymore, it is also not the answer we want.
After a lot of checking, I realised that when we need to find , we use the invMod() function. However, if we look at invMod():
xxxxxxxxxx61def invMod(self,n):2 gcd, x, y = self.eec(n)3 if gcd == 1:4 return x % self.p5 else:6 raise ValueError(str(p)+" isn't prime (or n = 0)")It returns x % self.p, whereas we want x % self.n. To fix this, I changed both invMod() and ecc() to take both n and p, where p is the value we want for the modulus. The new code looks like this:
xxxxxxxxxx201def eec(self,a,p):2 x, old_x = 0, 13 y, old_y = 1, 04 r, old_r = p, a56 while r != 0:7 quot = old_r // r8 old_r, r = r, old_r - quot * r9 old_x, x = x, old_x - quot * x10 old_y, y = y, old_y - quot * y11 12 # ax + by = gcd(a,b)13 # returns (gcd, x, y)14 return old_r, old_x, old_y15def invMod(self,n,p):16 gcd, x, y = self.eec(n,p)17 if gcd == 1:18 return x % p19 else:20 raise ValueError(str(p)+" isn't prime (or n = 0)")This new code is more correct. Unfortunately, the verify function still fails.
xxxxxxxxxx11False
This is very inconvenient to debug, as it is almost impossible to tell what the correct answers should be.
After hours of debugging, checking every single function - and rewriting half of them - I found the issue.
The problem was... a typo.
xxxxxxxxxx71def verifyMsg(self,msg,signature,q):2 r,s = signature3 z = int(sha256(msg),16)4 u1 = (self.invMod(s,self.n)*z) % self.n5 u2 = (self.invMod(s,self.n)*r) % self.n # r used to be z6 x,y = self.addPoints(self.multiPoints(u1,self.g),self.multiPoints(u2,q))7 return r % self.n == x % self.nI forgot to change u2 to take r instead of z. Finally, the function works:
xxxxxxxxxx11True
We can check that it rejects incorrect messages:
xxxxxxxxxx41private,public = ec.createKeys()2signature = ec.signMsg("hello",private)3print(ec.verifyMsg("hello",signature,public))4print(ec.verifyMsg("bean",signature,public))xxxxxxxxxx21True2False
In this section, I built a working Elliptic Curve Digital Signature Algorithm in Python. This should allow me to easily convert to Javascript in the implementation phase.
I learnt a lot about the mathematics behind not just ECDSA, but also about finite fields and modular arithmetic.
From here, we need to design the application serves as an interface to Arbitra's functions.
We need to figure out how to ensure that the client can connect to the network. The way most other cryptocurrencies handle this is firstly maintaining a list of recent connections, and reconnecting to them. However, the client needs to connect to it's first node. Bitcoin, for example, solves this by having a few trusted nodes hardcoded into the client, which in turn maintain a list of a few trusted nodes. What I believe would be best for Arbitra is to create and maintain our own list, but of course allow the client the option of adding their own nodes. Bitcoin then also has some websites that maintain their own list of trustworthy nodes, which clients can connect to.
We also need a system of node discovery. The client is never going to expand it's list of nodes if it only connects to the default nodes. A way that we could implement it is by having mined blocks optionally include their address, so that whenever a new block is mined, a new IP is broadcast eventually to the entire network so that client's list of nodes can expand over time. However, having everyone connect to the same node if the manage to mine a block is probably not the best idea.
Bitcoin, for a time used to run an IRC server where nodes could broadcast their IP for people to connect to. However, this feature was removed as it was not scalable and provided a single point of failure to an aspect of the network. Whilst it clearly is not the best idea (as they got rid of it) it could be a stop-gap solution if clients are having difficulty connecting to enough nodes.
For Arbitra, I am probably going to have the websites with a list of trusted nodes, and then also have some hardcoded nodes in the client. The client will then remember nodes that it connects to. Then, most importantly, it will have a message type that will allow clients to ask other clients to share their list of nodes. This way, a client can expand their list
Not only does the network need to be described in detail, but the application as well, as that is what will be used to interface with it. As previously mentioned, Electron will be used to create the application. Electron is a technology that embeds a webpage within a chromium instance, and also allows access to lower level OS functions.
This means that the UI is implemented in HTML/CSS, and the backend is done using Javascript.
There are three main parts of the application - creating transactions, viewing and interpreting the blockchain, and mining the blockchain. Therefore, the application should focus on these areas.
Something not yet mentioned is the ability to import and export wallets - users should be able to transfer their wallets from one computer or application to another.
The first thing I decided to do was to roughly sketch out how I wanted the application to look.
This concept was designed to be striking, with the purple backdrop drawing the eye. This was made thinking about how cryptocurrency wallet apps look like on mobile devices.

This concept was inspired by this redesign of Windows File Explorer, by Frantisek Mastil.


This is a purple/grey version of concept 2.

The first concept would be quite minimal, therefore there would only be three or so buttons that could be accessed from the main page. Therefore the structure of this app would be quite hierarchical.
However, the other two concepts are different. Since they both have a big menu on the left, most parts of the application can be accessed from every page. This makes the structure of the application much flatter.
The idea behind how I've structured the protocol is that there are different types of messages, each of which has a different specific reply. Each message is meant to stand alone by itself, and there is no 'conversation' between nodes. For example, if a node want's to check that it's blockchain is up to date, it will send out a message asking for the top block. If someone replies with their top block and it's different to what they have on disk, instead of replying to the same node asking for the blockchain, it will instead send out a chain request to all the nodes it is in contact with. This greatly simplifies complexity of the messaging system, allowing me to develop each function separately from each other.
So that messages in the network can be understood by everyone in it, we need to make sure that there are strict definitions on message types. If a node receives an invalid message, it will reject it.
Since this project will be written in Javascript, each packet will use the JavaScript Object Notation (JSON). A rough example of a JSON object looks like this:
xxxxxxxxxx81{2 id: "cad944434a29dcfcfb4080cec264396fd23d73c1708db39bd780e7e30ef9072f",3 sender: "4a57bd2226eb76cceddf0cfe0baa2a1391b4952db4610dc3762845cedffdff62",4 recipient: "aed1fe98cda4ba5a1681a19aa768f73b9d707c5621c7effdf2938e242080505e",5 amount: 50.0,6 timestamp: "1505052733",7 signature: "5773487d221545d26fd0f57fdb3a7d986bc479a850d7b0d762e8c7f4772790a0"8}This means that it will be much easier to parse the messages as they are already in a format that Javascript can use.
However, it would be strange to have all of the info in one big group - for example, when checking a hashed block, you would have to remove some of the block's values and then hash it which, while possible, is inefficient and overly complex. The solution to this is separating each message into a header and a body. The header contains info about the block - who it was send by, what it's hash is, what kind of message it is etc. The body contains the actual message.
This system is far better because it means that the system can simply check the standardised header to find out what the message is, rather than parse each type of message separately. Also, since a lot of the messages are hashed, we can simply hash the content and put the hash into the header, which makes a lot more sense than having to remove elements from the message before you can confirm the hash.
A message with a header would look like this:
xxxxxxxxxx151{2 header: {3 type: "block"4 sender: "168.123.421.9",5 hash: "0000000d221545d26fd0f57fdb3a7d986bc479a850d7b0d762e8c7f4772790a0"6 }7 body: {8 id: "cad944434a29dcfcfb4080cec264396fd23d73c1708db39bd780e7e30ef9072f",9 sender: "4a57bd2226eb76cceddf0cfe0baa2a1391b4952db4610dc3762845cedffdff62",10 recipient: "aed1fe98cda4ba5a1681a19aa768f73b9d707c5621c7effdf2938e242080505e",11 amount: 50.0,12 timestamp: "1505052733",13 signature: "5773487d221545d26fd0f57fdb3a7d986bc479a850d7b0d762e8c7f4772790a0"14 }15}We need to parse these messages, so I decided to plan the function that handles this in pseudocode.
xxxxxxxxxxFUNCTION parseMessage (message): TRY: STRING hash <- hash(message.body) IF (hash == message.body.hash): IF (message.header.type == 'transaction'): OBJECT reply <- parseTransaction(message) ELSE IF (message.header.type == 'block'): OBJECT reply <- parseBlock(message) ELSE IF (message.header.type == 'ping'): OBJECT reply <- parsePing(message) ELSE IF (message.header.type == 'node_request'): OBJECT reply <- parseNodeRequest(message) ELSE: THROW 'parseError' ELSE: throw 'hashError' CATCH (err): OBJECT reply <- parseError(err) FINALLY: reply.header.hash <- hash(reply.body) RETURN replyThis basically checks the message's type and calls the corresponding function. If there is an error, such as if the hash is invalid (indicating that the message has been tampered with) then it sets the reply to be an error message. Finally, regardless if an error was thrown, it sets the hash of the reply and returns the reply to be sent off.
So that it is easier to determine if a message is valid or not, we need to determine not only what the different messages are, but also the different formats that each part of the message will be (i.e. integer, hex string, enum). The way that I'll do this is with a table with the name, the type, and the maximum length.
The size does not apply to all types, mainly numbers. JS numbers are all 64bit double-precision floats, so it assumed they are that size.
The JSON object used in the first example above in this format would look like this:
| name | type | size |
|---|---|---|
| id | hex string | 64 |
| sender | hex string | 64 |
| recipient | hex string | 64 |
| amount | float | |
| timestamp | timestamp | |
| signature | hex string | 64 |
Please note this is an example and not the final definition of a transaction message.
Each message has a header. It has the following attributes:
| name | type | size |
|---|---|---|
| type | string | 2 |
| size | integer | |
| hash | hex string | 64 |
| version | string | |
| time | timestamp |
size is the size of the body in bytes. hash is the SHA256 hash of the body, and acts as both an identifier and as a checksum. If size becomes a concern, it may become necessary to truncate the hash. The version is the version number of the client.
However, not all message types have unique contents, and therefore their hashes would be the same, making the usage of the hash as a unique identifier. I initially thought that this can be fixed by appending the timestamp onto the end of the body before hashing it.
xxxxxxxxxx11hash = sha256(msg.body+msg.header.time)However, this does not work for message types where they are relayed (block and transaction). This is because the timestamp is in the header, which changes each time that it's sent, as well as other complications that I ran into. I realised that a better solution was to move the time into the body. This way, the body is always unique, and the hash can always be found by just hashing the body.
The message type is a string, and is one of:
| message type | string |
|---|---|
| Transaction | tx |
| Block | bk |
| Latest block hash request | hr |
| Chain request | cr |
| Ping | pg |
| Node request | nr |
For each of the message types, I not only wanted to document it's attributes, but also what the client will do when it receives it, so I made flowcharts.
A transaction, as mentioned earlier, has the sender, the recipient, the amount, the signature, and the time. However, it needs to be decided how to support multiple senders, so that in the case that someone need to send a large amount of Arbitrary Units, but had multiple wallets with that amount split between them, they would not need to clutter the blockchain consolidating funds before sending the transaction.
One way could be to have two lists, one with the recipient's names and one with the value they are given:
xxxxxxxxxx71{2 sender: "0980192830198019",3 amounts: [10, 4]4 signatures: ["19321092","12301932019"]5 recipient: "1239817320812033987193",6 7}This is a good method as it allows you to clearly see the list of recipients.
Unfortunately, this is not very clear which amount is being taken from each wallet. It could also be vulnerable to the list's order being confused. It also does not support transaction fees (unless, for example, if the list of amounts is great than the list of recipients, the remaining amounts are given to the miner).
Another way of doing it could be to have a list of object literals:
xxxxxxxxxx41{2 to: "109230918093"3 from: [{person:"091230192012", amount:10, signature:"123129"}, {person:"109127981737", amount:4,signature:"792873928"}],4}A benefit of doing it this way is that it saves putting the total amount as a separate value. However, this then means that the ability to add a transaction fee is lost without having it as a different value.
Between the two, I would say that the second method is better, because it explicitly binds the recipients to the amounts they get. However, the other option is still available if, for whatever reason, I can't use the preferred method. I also aim to get rid of transaction fees, so not being able to implement that is fine.
Finally, a wallet is a public key, which is a point on the curve. We can convert them to hex and concatenate them into a big hex string, which should be 128 bytes long.
From this, we can make our table for transactions:
| name | type | size |
|---|---|---|
| to | hex string | 128 |
| from | array | |
| time | timestamp |
The from array would contain objects of the form:
| name | type | size |
|---|---|---|
| wallet | hex string | 128 |
| amount | integer | |
| signature | hex string | 128 |
The signing mechanism, as covered earlier, takes an input string and a private key, and produces a signature. However, the input string can't just be the amount, as then the signature can be reused. It also can't just be the amount plus the public key it's addressed to, otherwise an attacker could repeatedly send the same transaction. Therefore, we need to sign the amount concatenated with the public key concatenated with the current time. Since all of these are available in the transaction message itself, the signature can still be verified, but it also can't be duplicated.
Something to note about floating-point operations is that they're not very accurate.
xxxxxxxxxx21>>> 0.1+0.220.30000000000000004This is not good for handling transactions like this. We can circumvent this, however, using integers, and then multiplying them by a fixed amount. I decided that the smallest unit an arbitrary unit could be split up into is a micro-Arbitrary unit, or . Then, we don't have to deal with floating point operations, as we can just use integers to define the number of au, which we can then convert back to au. For example, to send 50au, the amount would be written as 5000000.
The flowchart looks like this:
Blocks contain transactions, which are objects. We also need to determine the method whereby the miner receives their reward, and the simplest way of doing that is to have a miner attribute in the body in which the person who mined the block can put any public key they desire, and it will be rewarded 50au.
| name | type | size |
|---|---|---|
| transactions | array | |
| miner | hex string | 128 |
| nonce | string | |
| difficulty | integer | |
| parent | hex string | 64 |
| time | timestamp | |
| height | integer |
The transactions array would contain the body of transaction messages. The nonce can be any string that makes the hash of the body meet the required difficulty. The difficulty is determined by how long it took to mine the previous block, but the mechanisms behind that works will have to be determined by testing once block mining is implemented. parent is the hash of the block that comes before it in the chain. height is the number of blocks it is away from the genesis block.
The ping message is a critical part of the network. Sending a ping signals that the node wants to be sent messages that other nodes receive. It also has a Boolean value advertise, which means that, if set to true, the node that received the ping will send the IP address of the node that sent it to nodes that send a node request. This should be a toggle that the client can switch if they receive too many messages.
| name | type | size |
|---|---|---|
| advertise | boolean | |
| time | timestamp |
On receiving a ping, a client will reply with a ping. This shows that they acknowledge each other.
Sending a node request asks for the list of recent connections that each client maintains, provided that the connection has marked itself as willing in the ping message. A node request has an optional value defining the maximum number of nodes that it wants to receive (blank for all).
| name | type | size |
|---|---|---|
| max | integer | |
| time | timestamp |
This is a small message that a client sends out to check that it's blockchain is up to date.
| name | type | size |
|---|---|---|
| time | timestamp |
This function asks other nodes for the chain beneath the hash listed in the body. This is typically sent after a hash request.
| name | type | size |
|---|---|---|
| hash | hex string | 64 |
| time | timestamp |
Not only are there messages that are sent out by the client, but there are also the different types of message that are sent back in reply to these messages. Some we have already touched on, such as the block and the ping. However, some messages don't need a specific reply, so they have a generic "received" message. Each reply corresponds to a message:
| name | reply to | string |
|---|---|---|
| Ping | pg | pg |
| Chain | cr | cn |
| Block Hash | hr | bh |
| Node | nr | nd |
| Received | tx, bk | ok |
The ping reply is just another ping.
In reply to a chain request. It is just an array of blocks block and it's associated hash.
| name | type | size |
|---|---|---|
| chain | array | |
| time | timestamp |
When a client receives this message, it verifies each one and if it passes, it adds it to the blockchain.
In reply to a block hash request. It is simply the hash from the top of the blockchain.
| name | type | size |
|---|---|---|
| hash | hex string | 64 |
| time | timestamp |
When a client receives a block hash, it checks it to see if it is the same as their top block. If it is not, it sends a chain request to all connections.
In reply to a node request. This is simply an array of nodes from the list of recent connections that the client maintains that have marked themselves as willing to be broadcast across the network.
| name | type | size |
|---|---|---|
| nodes | array | |
| time | timestamp |
The array is just an array of strings.
When a client receives this message, it sends a ping to each of the IP listed in the message.
The received message type is just a confirmation that the message had been received and accepted. Therefore, the only thing in the body is the timestamp.
| name | type | size |
|---|---|---|
| time | timestamp |
As well as correct replies, we need to messages that a client will reply with if the messages received is incorrect in some way. The message will have type er, for error. It also contains the hash of the failed message, if available.
| name | type | size |
|---|---|---|
| error | string | 20 |
| hash | hex string | 64 |
The error can be one of several strings, that correspond to different errors.
| error string | description |
|---|---|
| parse | Failed to parse JSON - message is not valid JSON |
| hash | Hash does not match |
| signature | Signature is invalid |
| type | The type of the message is invalid |
| amount | Transaction invalid due to not enough funds |
| transaction | Transaction in a block is invalid |
| notfound | Requested block/data not found |
In order to gain an understanding about how the network would function, I decided to model a network with a limited number of nodes, manually.
I set up the network in this way so that network traffic has to disperse through the network through multiple nodes. This simulates a real network, where not every node will be connected to every other node.
The rule is that when a node receives a message, it passes it on to every node that it is connected to.
The arrows indicate that the message is passed from the node on the left to the node on the right, and the lines indicate a step. The nodes listed after the line are the nodes that have received the message
xxxxxxxxxx1511 ---------- S12S1 > S23S2 > S34S1 > S552 ---------- S1, S2, S3, S56S2 > S17S2 > S38S2 > S49S3 > S110S3 > S211S3 > S512S5 > S113S5 > S314S5 > S415S5 > S6
I'm going to stop the simulation at the second state, because I can see a loop is starting to form. Since S2 is connected to S1, it will send the message back, which in turn will send the message back, forever.
This can be fixed with a single rule: No passing messages back to the node that sent it.
However, there is another issue - in a loop of three nodes like S1, S2, and S3, the can still be a loop if a message gets passed from S1 to S3 via S2. Since S3 doesn't know that S1 sent the message, it can send it to S1 which will send it around the loop again. Therefore, we need a second rule: Never send the same message to the same node twice. This, in the actual network, would probably mean keeping track of the list of messages that have passed through the node via their hash, and not repeating an incoming message if it's hash is on this "blacklist".
xxxxxxxxxx1411 ---------- S12S1 > S23S2 > S34S1 > S552 ---------- S1, S2, S3, S56S2 > S17S2 > S38S2 > S49S3 > S210S3 > S511S5 > S312S5 > S413S5 > S6143 ---------- S1, S2, S3, S4, S5, S6
Since the application will use Electron, the frontend is made with HTML/CSS. Because I have experience with web design I decided to prototype the UI using a static HTML page.
The code used to create the UI is located at the bottom of this section.
I decided to go with concept 1.

This first pass, while it captures the aesthetics that I'm going for, does not have the functionality I want. There are no buttons, and I want a visually striking graph or other graphic to fill the space beneath the current balance and the important buttons.

In this updated prototype, I remove the placeholder text, as there is no need for text anyway, and moved the existing information boxes to the bottom. I then added the most important buttons - making a transaction, mining the blockchain, and settings. Beneath those is a temporary graph (which can be found here). The proper graphic, whatever it turns out being, will be the same grey colour with purple highlights.
This update is an improvement, but it is still missing some features. The most important being a way to access the transaction history and other minor features. However, I don't want to clutter the top of the page. In a normal website, this would probably be hidden in a sidebar menu or something similar, but since this is pretending to be a desktop app that would be out of place.
The solution I came up with was to create a preview list of previous transactions. Through this approach, there is more information available at a glance to the user, it adds more functionality to the homepage, and it creates a non-intrusive way to add the link to the list of previous transactions.

I think it works quite well. You would be able to switch between all recent transactions, all recent outbound transactions, and all recent inbound transactions, and also be able to navigate to a more in-depth page.
I also made some slight adjustments to the spacing to make it more consistent, as well as adding a 0.5 second easing animation effect to all background transitions using transition: background-color 0.5s ease.
Something that occurred to me when using other cryptocurrencies is that it is often unclear when the application is downloading the blockchain. I therefore had the idea to turn the purple banner at the top of the application into a huge loading slider, like so (mockup):

I had to manually position the 45% as it was a child of the progress bar itself, so this is obviously a temporary solution. However, if I have time it would be a cool way to incorporate the banner into the functionality of the application.
I really don't like the way this it turning out. It feels more like a website than an app, and with this in mind I decided to try to rearrange it into concept 2, from the analysis. With this second attempt I took into consideration:

I think that this is a vast improvement. It now looks much more like a traditional desktop application. The colours are much more professional and the way you navigate through the app is much more streamlined and easy to understand.
I also made the drag region a separate div. I highlighted it here in red:

After some minor polish, this is the final concept:

xxxxxxxxxx401<html>2 <head>3 <meta charset="utf-8">4 <title>Arbitra Client</title>5 <link rel="stylesheet" href="style.css"/>6 <script src="https://use.fontawesome.com/c7895c8683.js"></script>7 </head>8 <body>9 <div class="left">10 <h1 class="money">10000</h1>11 <ul>12 <li><i class="fa fa-rss" aria-hidden="true"></i> 5 connections</li>13 <li><i class="fa fa-link" aria-hidden="true"></i> 11832 chain length</li>14 <li><i class="fa fa-clock-o" aria-hidden="true"></i> 5 seconds since last block</li>15 </ul>16 <div class="subsec">Overview</div>17 <div class="subsec">Transactions</div>18 <div class="items">Make Transactions</div>19 <div class="items">Recieve Transactions</div>20 <div class="items">Transaction History</div>21 <div class="subsec">Blockchain</div>22 <div class="items">View Blockchain</div>23 <div class="items">Mine for Arbitrary Units</div>24 <div class="subsec">Settings</div>25 <div class="items">Network Settings</div>26 <div class="items">Application Settings</div>27 </div>28 <div class="right">29 <div class="dragbar"></div>30 <div class="closebox">31 <i id="min" class="fa fa-window-minimize" aria-hidden="true"></i>32 <i id="max" class="fa fa-window-restore" aria-hidden="true"></i>33 <i id="close" class="fa fa-window-close" aria-hidden="true"></i>34 </div>35 <div id="body">36 <h1>Overview</h1>37 </div>38 </div>39 </body>40</html>The HTML structure is split into three parts - the left menu, the drag bar, and the main section. They are given the class names .left, .dragbar, and #body, respectively. #body and .dragbar are grouped into a div called .right, to ensure that they are kept together.
The menu in .left is made up of a series of divs called .subsec and .items. There is probably a better way of doing this, but I was struggling to align list elements so I decided to use divs. .subsec describes the menu subsections, and .items describes the different links - these will have to be buttons eventually, but for now they are fine.
The drag bar is made of two elements - .dragbar, which is what you can use to drag the window around, and .closebox, which contains minimise, maximise, and close buttons - they are now Font Awesome icons. This is because if the entire top section is a draggable region, you are not able to click the close buttons. To solve this, I made sure that the two elements do not overlap.
xxxxxxxxxx971* {2 font-family: Segoe UI, Helvetica, sans-serif;3 margin: 0;4 padding: 0;5 transition: background-color 0.5s ease;6}78body {9 height: 100vh;10 overflow: hidden;11}1213.money::after {14 content: "au";15 font-size: 0.65em;16}1718.left {19 width: 300px;20 height: 100%;21 float: left;22 padding-top: 20px;23 background-color: #333;24}2526.left > * {27 color: #fdfdfd;28 list-style: none;29 padding-left: 25px;30}3132.left > .items:hover {33 background-color: rgba(43, 43, 43, 0.5);34}3536.left > .subsec:hover {37 background-color: rgba(43, 43, 43, 0.5);38}3940.left > h1 {41 font-size: 3em;42}4344.subsec {45 font-size: 1.5em;46 padding: 3px;47 padding-left: 25px;48 margin-top: 15px;49}5051.items {52 font-size: 1.2em;53 padding: 3px;54 padding-left: 35px;55}5657.right {58 width: calc(100vw - 300px);59 height: 100vh;60 background-color: #fdfdfd;61 float: left;62}6364.dragbar {65 width: calc(100% - 150px);66 height: 50px;67 float: left;68}6970.closebox {71 width: 140px;72 height: 40px;73 float: left;74 padding: 5px;75}7677.closebox > i {78 font-size: 2em;79 padding-left: 11px;80 color: #333;81 cursor: pointer;82}8384#close:hover {85 color: red;86}8788#body {89 width: calc(100% - 20px);90 padding: 10px;91 color: #333;92}9394#body > h1 {95 font-size: 2.5em;96 border-bottom: 2px solid #333;97}For the CSS, I made use of some cascading, for example making all the icons in the .closebox div have the same colour, size and padding, without having to give them their own class.
I also used the calc() function a lot as it allowed me to, for example, make the .dragbar full width minus the size of the close buttons.
To get the "au" suffix on the number on the top, I used a ::after tag with content: "au"; and a different size. This meant that every element with the .money class is automatically followed by "au", which is very useful as if I have a text element that I know will contain an amount of currency, I can give it this class rather than having to manually append "au" in the backend.
For the colours, I generally used #333 (grey) and #fdfdfd (off-white). The font is Segoe UI.
Now that we have the overall style decided upon, we now need to flesh out all the UI elements that we need. This includes:
Since the page is pretty much already blank, all I did was create a new file and copy the HTML code over.
To start, I listed out most of the HTML elements that we might need.
xxxxxxxxxx311 <div class="body">2 <h1>Testing Page</h1>3 <h2>Subheading</h2>4 <h3>Subsubheading</h3>5 <h4>Subsubsubheading</h4>6 <h5>Subsubsubsubheading</h5>7 <h6>Subsubsubsubsubheading</h6>8 <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce nec ex congue ligula pretium vulputate sit amet vel enim. Proin tempor lacinia dui, sit amet maximus velit commodo ut. Etiam nulla justo, cursus eu risus in, interdum convallis nibh. Phasellus non rutrum tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec viverra nec est vitae mollis. Donec pharetra nisl enim, vitae efficitur arcu rutrum non. Phasellus id congue mi. Integer sagittis diam sed luctus dapibus. Proin at feugiat sapien. Nam ac mauris vitae massa placerat ultrices in eget odio. Nunc vehicula neque ut neque molestie, a tempor massa viverra.</p>9 <p>Aliquam efficitur cursus sollicitudin. Nulla eu diam in ex commodo aliquet in sit amet augue. Cras maximus auctor dui eu imperdiet. Suspendisse et sem est. Nunc lectus dui, accumsan nec lacus in, auctor pharetra purus. Nullam eu nisi porttitor, tempor nibh nec, accumsan velit. Vestibulum posuere erat et placerat lacinia. Nulla eget fermentum arcu. Quisque aliquam tellus id felis pharetra sagittis. Donec venenatis ligula nibh, nec dignissim magna lobortis mattis. Aliquam ultrices orci et pretium feugiat.</p>10 <ul>11 <li>Unordered</li>12 <li>List</li>13 </ul>14 <ol>15 <li>Ordered</li>16 <li>List</li>17 </ol>18 <form>19 <input type="text" placeholder="Text field"><br>20 <select name="dropdown">21 <option>Dropdown</option>22 <option>List</option>23 <option>Options</option>24 </select><br>25 <input type="radio"> Radio<br>26 <input type="checkbox"> Checkbox<br>27 <input type="submit" value="Submit">28 </form>29 <button>Button</button>30 <div class="highlight">Highlighted area</div>31 </div>
As you can see, since I removed all padding and margins from every element at the start of the CSS page, the p elements lack any separation between paragraphs, and the lists are not indented as they should be.
Furthermore, the buttons are very ugly by default in Chrome, and do not fit with the style at all.
However, they all inherit the correct colours from the #body div, which is what we want.
My first attempt to fix this was to add back the default padding and margins.

This completely broke it, as many HTML elements have default margins that I was relying on not existing. However, if the margin is set to 0 but we don't change the padding, it fixes the unordered and ordered lists.

The next step is to fix the p tags.
xxxxxxxxxx41p {2 margin-top: 2px;3 margin-bottom: 5px;4}
At this point, I think that basic elements like headings and paragraphs are fine, so I will remove them from the testing page. The next thing to do is to try to style the buttons, as they are an important part of the visual theme of the application.
xxxxxxxxxx181button, input[type=submit] {2 background-color: #fdfdfd;3 font-weight: 400;4 margin: 2px 0;5 min-width: 70px;6 padding: 3px 10px;7 border: 1px solid #333;8 border-radius: 15px;9}1011button:hover, input[type=submit]:hover {12 background-color: #333;13 color: #fdfdfd;14}1516h1 {17 margin-bottom: 8px;18}
I decided upon a capsule-shaped button with very rounded corners and a thin, 1px thick border. The second button is what it looks like when hovered over - it transitions to a dark background to signify to the user that it is a button. The transition is achieved by applying transition: background-color 0.5s ease and transition: color 0.2s ease to * (all elements).
I also applied margin-bottom: 8px to h1 as it was touching the text field.
xxxxxxxxxx151input[type=text], input[type=number] {2 background-color: #fdfdfd;3 font-weight: 400;4 margin: 2px 0;5 width: calc(100% - 20px);6 max-width: 300px;7 padding: 3px 10px;8 border: 1px solid #333;9 border-radius: 15px;10}1112input[type=text]:focus, input[type=number]:focus {13 border-radius: 5px;14 outline: none;15}
I found that much of the styles applied to the buttons could be also applied to the text field. As with the buttons, the second field ("Number field") has been focus. As you can see, the focused field as slightly less rounded corners, so that it is clear which field has been selected.
I am not 100% happy with how it has turned out, but it is fine for a first pass.
It turns out that to create custom radio buttons and checkboxes requires replacing them completely, and since I don't think that I'll use them much they are fine having the default look.
At this point I added a hyperlink to the testing page, and changed the max-width of the text fields to 278px so that they are the same size as the dropdowns.
xxxxxxxxxx111.highlight {2 background-color: #333;3 display: inline;4 color: #fdfdfd;5 border-radius: 3px;6 padding: 1px 3px 3px; 7}89p > a {10 color: #666;11}
For the highlighted area I decided to go with a #333 background and rounded edges, and for the hyperlink I decided to just lighted it slightly and keep the underline to differentiate it from the rest of the text. I also used p > a to select it, as it then only selects hyperlinks that are in text as otherwise it could accidently apply the background and padding to a div or button that is wrapped in a hyperlink, which would cause issues.
xxxxxxxxxx81p > a:hover {2 color: #fdfdfd;3 background-color: #333;4 padding: 0 2px 2px;5 margin: 0 -2px -2px;6 text-decoration: none;7 border-radius: 3px;8}
Using the :hover pseudo-class, the hyperlink has a dark background when hovered over with the mouse. I used negative margins to ensure that the increased padding did not change the size of the element, as otherwise it would shift around on the page when hovered over.
There are multiple instances in the application where we will need to display a list of information. I decided to implement this using HTML tables. Here is a basic table with some arbitrary data:
xxxxxxxxxx331<h2>Sent Transactions</h2>2<table>3 <tr>4 <td>Recipient</td>5 <td>Hash</td>6 <td>Amount</td>7 <td>Timestamp</td>8 </tr>9 <tr>10 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>11 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>12 <td>200</td>13 <td>1505052733</td>14 </tr>15 <tr>16 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>17 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>18 <td>200</td>19 <td>1505052733</td>20 </tr>21 <tr>22 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>23 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>24 <td>200</td>25 <td>1505052733</td>26 </tr>27 <tr>28 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>29 <td>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</td>30 <td>200</td>31 <td>1505052733</td>32 </tr>33</table>As you can see, the hashes are very large. We need to make sure that it doesn't push the rest of the contents off of the screen. In this case, tables may not be the best option. Perhaps it would be better to simply do it using divs, as then we can make sure it is responsive.

There is also the issue that transactions can have multiple recipients. To account for this, I created a list object that would be repeated. I also used wallets instead of transactions, but we can reuse the same styles on the transaction page.
xxxxxxxxxx281<h2>Wallets</h2>23<div class="highlight-box">4 <h3>My wallets</h3>5 <button>Create new wallet</button>6 <div class="list">7 <div class="list-item">8 <p>Wallet name</p>9 <p>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</p>10 <p><span class="money">0</span></p>11 </div>12 <div class="list-item">13 <p>Wallet name</p>14 <p>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</p>15 <p><span class="money">0</span></p>16 </div>17 <div class="list-item">18 <p>Wallet name</p>19 <p>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</p>20 <p><span class="money">0</span></p>21 </div>22 <div class="list-item">23 <p>Wallet name</p>24 <p>3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb</p>25 <p><span class="money">0</span></p>26 </div>27 </div>28</div>This produces:

This is much better looking, and much more flexible than the table approach. It is much simpler than the table, consisting of an outer div, then the .list div which contains all the .list-items. .list-item is very simple, as it just contains three p tags on top of each other.
xxxxxxxxxx311.highlight-box {2 margin-top: 20px;3 width: calc(100% - 20px);4 position: relative;5 border-radius: 5px;6 padding: 10px 10px 5px;7 max-height: 70vh;8 background-color: #ececec;9 border: 1px solid #333;10}1112.highlight-box > button {13 position: absolute;14 top: 5px;15 right: 10px;16}1718.list {19 overflow-y: auto;20 overflow-x: hidden;21}2223.list-item {24 overflow-x: auto;25 width: calc(100% - 12px);26 background-color: white;27 margin: 5px 0;28 padding: 5px;29 border: 1px solid #333;30 border-radius: 5px;31}In order to start we need to create the Electron app. Since this is the first time I have used Electron, I followed the Electron Quick Start guide:
https://electron.atom.io/docs/tutorial/quick-start/
First, we need to create the package.json file. This contains information about the application and where the main Javascript file is, in order to run it.
xxxxxxxxxx51{2 "name": "arbitra-client",3 "version": "0.1.0",4 "main": "main.js",5}We then need to make the main.js files in order to create the window. This is mainly boilerplate code which won't change for our purposes, taken from the Electron Quick Start guide.
xxxxxxxxxx521const {app, BrowserWindow} = require('electron')2const path = require('path')3const url = require('url')45// Keep a global reference of the window object, if you don't, the window will6// be closed automatically when the JavaScript object is garbage collected.7let win89function createWindow () {10 // Create the browser window.11 win = new BrowserWindow({width: 1280, height: 720})1213 // and load the index.html of the app.14 win.loadURL(url.format({15 pathname: path.join(__dirname, 'index.html'),16 protocol: 'file:',17 slashes: true18 }))1920 // Open the DevTools.21 //win.webContents.openDevTools()2223 // Emitted when the window is closed.24 win.on('closed', () => {25 // Dereference the window object, usually you would store windows26 // in an array if your app supports multi windows, this is the time27 // when you should delete the corresponding element.28 win = null29 })30}3132// This method will be called when Electron has finished33// initialization and is ready to create browser windows.34// Some APIs can only be used after this event occurs.35app.on('ready', createWindow)3637// Quit when all windows are closed.38app.on('window-all-closed', () => {39 // On macOS it is common for applications and their menu bar40 // to stay active until the user quits explicitly with Cmd + Q41 if (process.platform !== 'darwin') {42 app.quit()43 }44})4546app.on('activate', () => {47 // On macOS it's common to re-create a window in the app when the48 // dock icon is clicked and there are no other windows open.49 if (win === null) {50 createWindow()51 }52})Then we need to include the HTML and CSS files. For this I used the HTML and CSS code from the concept in the UI Design section. I named these files index.html and style.css, respectively, and placed them in the same directory as the other two files.
Finally, we need to install Electron. Since I already had Node.js installed, I used the Node Package Manager to install it. After navigating to the directory where the rest of the project was using cd <filename>, I used the command npm install --save electron. Using the --save tag ensures that it is saved in the node_modules directory it creates.

It successfully installed. That image is just the top section of the install process as it outputs a large amount of irrelevant data. All that is left is to start up Electron. According to the Electron Quick Start Guide, the command to do this (from the directory) is .\node_modules\.bin\electron ..

A few seconds later, the window appeared!

The default "chrome look" of the window is quite ugly. Luckily, it is quite simple to remove. We simply change
xxxxxxxxxx41function createWindow () {2 // Create the browser window.3 win = new BrowserWindow({width: 1280, height: 720})4 to
xxxxxxxxxx41function createWindow () {2 // Create the brower window.3 win = new BrowserWindow({width: 1280, height: 720, frame: false})4 This changes the window to look like this, which is a vast improvement.

However, we now can't drag the window around, or close it. We need to be able to use the top area to move the window, and implement our own minimise and close buttons. For the time being, I will just implement the close button to avoid needing to find icons etc.
Luckily, dragging the window is easy - we add a property to the CSS called -webkit-app-region.
We want to make the purple area draggable, so we add -webkit-app-region: drag to .dragbar. This works, but I can't really demonstrate this with an image.
We now need it to close when it is clicked. My first attempt was to add the function call onclick="closeWindow()" to the HTML, and in main.js add the closeWindow() function:
xxxxxxxxxx31function closeWindow () {2 win.close()3}However, this does not work. It turns out that, because Electron is based on the Google Chrome browser, there are two types of process: the main process and the renderer processes. The main process is like Chrome itself - it is a kind of background process. Then, each tab has it's own renderer process. This is why when you close a Chrome tab it doesn't close down Chrome itself. In our app, main.js is the main process, and in order to interact with the window to close it and do other functions, we need to create a renderer process for the main page. This file is the equivalent of the client-side Javascript that it normally used in front-end web development.
Therefore, I created renderer.js, and imported into the HTML at the bottom of the body.
xxxxxxxxxx11<script src="renderer.js"></script>In renderer.js, we need to import a module in Electron called remote. remote is how to interface with the window. Then, we make the document call a function when the "ready state" changes. This function checks if the ready state is ready - effectively, this means that everything inside the if statement will only be called when the document is ready. This is necessary as if we try to select an element before it is ready, it would not work, meaning that it could leave the close button without an event listener
xxxxxxxxxx71const remote = require('electron').remote23document.onreadystatechange = function () {4 if (document.readyState == "complete") {56 }7}With that done, we can add the event listener, which uses remote to get the current window, and then closes it.
xxxxxxxxxx101const remote = require('electron').remote23document.onreadystatechange = function () {4 if (document.readyState == "complete") {5 document.getElementById("close").addEventListener("click", function (e) {6 const window = remote.getCurrentWindow()7 window.close()8 })9 }10}This works - we can now close the app using the x.
We can use this to create minimise and maximise functions:
xxxxxxxxxx221const remote = require('electron').remote23document.onreadystatechange = function () {4 if (document.readyState == "complete") {5 document.getElementById("min").addEventListener("click", function (e) {6 const window = remote.getCurrentWindow()7 window.minimize()8 })9 document.getElementById("max").addEventListener("click", function (e) {10 const window = remote.getCurrentWindow()11 if (window.isMaximized()) {12 window.unmaximize()13 } else {14 window.maximize()15 } 16 })17 document.getElementById("close").addEventListener("click", function (e) {18 const window = remote.getCurrentWindow()19 window.close()20 })21 }22}The minimise function is simple, as it simply calls window.minimize() to minimise it, which is a function already in Electron. However, it is less simple for maximising the window, as the same button must also return the window to it's old size. For this we call window.isMaximized() to see if the window is maximised. If it is, we call window.unmaximize(), otherwise we call window.maximize().
The solution to this problem used this site: http://mylifeforthecode.com/making-the-electron-shell-as-pretty-as-the-visual-studio-shell/
We obviously need to be able to change pages in the app, and so for this we will be creating a testing page. I copied the HTML code to a new page called testing.html. I then added to .left the following code:
xxxxxxxxxx11<a href="testing.html"><div class="subsec">Testing</div></a>This should, when clicked, change the page from index.html to testing.html.
And it does. However, there is a problem, in that the app freezes and goes completely white for a second while the new page loads, which is unacceptable. This is because Electron is intended to host Single Page Applications, which load all the content at once then swap pieces out instead of completely reloading the page. This means that we need to create a system for doing this. Luckily, the app is set up in such a way that we have a div that we can change, and then keep everything else the same.
As it would be a real pain to keep everything in the index.html file and hide/unhide sections as we need them, I think that the best way of doing this would be to keep each page in a separate HTML file and then import them into index.html as needed using Javascript.
We can use the innerHTML() function in Javascript to do this. It takes a string, and when you apply it to an element it sets the children of the content to what the string is.
Before we do this, we need to set up the page we intent to change. First is the Overview - the main page. I created a new file called overview.html, and put it in a new directory called pages.
xxxxxxxxxx31<h1>Overview</h1>23<p>Welcome to Arbitra! This is placeholder text.</p>I also changed testing.html and moved it to pages.
xxxxxxxxxx31<h1>Testing Page</h1>23<p>This is placeholder text for the testing page.</p>I decided to plan this function using pseudocode.
x
FUNCTION changePage (page): STRING path <- 'pages/'+name STRING page <- readFile(path+'.html') // this is much too Javascript for pseudocode, but // otherwise it's needlessly confusing document.getElementById('body').innerHTML = page STRING code <- readFile(path+'.pseudocode') TRY: execute(code) CATCH: OUTPUT 'error opening page!'Now, we need to create the function that changes pages. In renderer.js:
xxxxxxxxxx251const remote = require('electron').remote2const fs = require("fs")34function changePage(name) {5 var path = "\\pages\\"+name+".html";6 fs.readFile(path, 'utf-8', (err, data) => {7 if (err) {8 alert("An error ocurred reading the file :" + err.message)9 console.warn("An error ocurred reading the file :" + err.message)10 return11 }12 console.log("Page change: "+name)13 document.getElementById("body").innerHTML = data14 });15}1617document.onreadystatechange = function () {18 if (document.readyState == "complete") {19 // Close Buttons go here2021 // Changing pages22 // Default is overview23 changePage("overview")24 }25}First we import the file system library, from Node.js, and set it to fs.
The changePage() function takes in the name of the page it changes to. It then creates the path name of where the pages are kept. It then uses fs.readFile to attempt to read the file.
If there's an error, it prints the error both in an alert and in the console. Otherwise, it prints that the page is changing, and then sets the innerHTML of #body to the data that that fs.readFile read.
However, there is a problem. When I tried to run this code, it threw an error.

This is because it tried to read the file C:\pages\overview.html rather than arbitra-client\pages\overview.html . We can fix this by changing
xxxxxxxxxx11var path = "\\pages\\"+name+".html"to
xxxxxxxxxx11var path = "pages\\"+name+".html"This makes it a relative path rather than an absolute path, and so should now link to the correct file.

Trying again, the application loads properly. This means the function works. We now need to turn the .items in the menu to buttons that change the pages. We can implement this using document.getElementById(). () => {...} is "arrow notation" for a function - it is a shorter way of writing function() {...}.
xxxxxxxxxx101 // Changing pages2 // Default is overview3 changePage("overview")45 document.getElementById("overview").addEventListener("click", () => {6 changePage("overview")7 })8 document.getElementById("testing").addEventListener("click", () => {9 changePage("testing")10 })I gave every menu item an ID, so that they can be selected.
xxxxxxxxxx121 <div class="subsec" id="overview">Overview</div>2 <div class="subsec">Transactions</div>3 <div class="items" id="make">Make Transactions</div>4 <div class="items" id="receive">Receive Transactions</div>5 <div class="items" id="history">Transaction History</div>6 <div class="subsec">Blockchain</div>7 <div class="items" id="view">View Blockchain</div>8 <div class="items" id="mine">Mine for Arbitrary Units</div>9 <div class="subsec">Settings</div>10 <div class="items" id="network">Network Settings</div>11 <div class="items" id="app">Application Settings</div>12 <div class="subsec" id="testing">Testing</div>When we click the Testing button, it changes page. This means that the navigation works, and we can massively simplify the process of creating and changes pages.

I also created a folder called js, where we can keep per-page Javascript files. Then in the changePage function, it imports the page's JS file, then calls a function which initialises the page. We use exports to expose functions in the file, in this case init(). In each per-page JS file (for example testing.js):
xxxxxxxxxx51function init() {2 console.log("Hello world!")3}45exports.init = initThen in changePage():
xxxxxxxxxx141function changePage(name) {2 var path = "pages\\" + name + ".html"3 fs.readFile(path, 'utf-8', (err, data) => {4 if (err) {5 alert("An error ocurred reading the file :" + err.message)6 console.warn("An error ocurred reading the file :" + err.message)7 return8 }9 console.log("Page change: " + name)10 document.getElementById("body").innerHTML = data11 const pageJS = require("./js/" + name + ".js")12 pageJS.init()13 })14}xxxxxxxxxx21Page change: testing2Hello world
It works. This means that we can properly separate the Javascript code into files.
I noticed when implementing the menu that the highlighting on the menu items was inconsistent, as it only applies to the .items and not the .subsecs. However, not all the .subsecs are divs, which means that we can't just apply the highlighting to them as well. We can simply fix these problems by giving all the .items and .subsecs that are meant to be divs a new class, called .link. We then give the highlighting properties to the .link class.
Replacing .left > .items:hover:
xxxxxxxxxx41.link:hover {2 background-color: rgba(43, 43, 43, 0.5);3 cursor: pointer;4}I also added cursor: default to .subsec. This means that the cursor when hovering over the menu items is consistent depending on whether it is a link or not.
crypto is a default module within Node.js that provides cryptographic functions, similar to Python's hashlib. They both provide wrappers around OpenSSL, which means that translating Python code from the experimentation phase should be fairly simple.
The first test is to hash a string using SHA-256. In order to do this in Javascript we need to first import crypto into the project. In main.js:
xxxxxxxxxx11const crypto = require('crypto')This imports the crypto object and sets it as a constant.
The next step is to hash a string. Using the documentation, I wrote this code to hash the string "something":
xxxxxxxxxx71// crypto testing23var data = "something"45var hash = crypto.createHash('sha256')6hash.update(data)7console.log(hash.digest("hex"))According to the experimentation code in Python, this should yield the hash 3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb
xxxxxxxxxx31C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client>.\node_modules\.bin\electron .233fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cbIt produced the same hash, which means that it works. Now, it would be good to put that into a function so that we can call it whenever we need to hash something. I also moved the function to renderer.js, as that is where it would be called.
xxxxxxxxxx51function sha256(data) {2 // creates a sha256 hash, updates it with data, and turns it into a hexadecimal string3 var hash = crypto.createHash('sha256').update(data).digest("hex")4 return hash5}As you can see, I've compacting the hashing to one line - it is still quite readable, and of course produces the same output.
A fundamental part of Arbitra is the Arbitra Network - the application by itself means nothing if it cannot connect to others. This means that we need to implement a reliable peer-to-peer networking system.
The way Arbitra will implement this is using the net Node.js module. From it's documentation:
The
netmodule provides you with an asynchronous network wrapper. It contains methods for creating both servers and clients (called streams).
Because the internet is centered on a client-server model, where clients request and receive data from a central server, most of the Node.js documentation and functions were focused on this.
However, even though we want a peer-to-peer network, we can still use this model. The client can run a server, and when another client wants to send it data it can connect to the server as a client. The first client can reply in this connection, but it will then be cut off. If the first client wants to send something else to the second client it connects using their server.
We can simulate this over localhost, the computer's local network. We don't even need to run the client and server in a separate file in order to simulate the model. The two upcoming snippets are in renderer.js.
xxxxxxxxxx121const net = require("net")2var server = net.createServer(function(socket) {3 console.log("Server created")4 socket.on("data", function(data) {5 console.log("Server received: " + data)6 var hashed = sha256(data)7 socket.write(hashed)8 })9 socket.on("end", socket.end)10})1112server.listen(80)First, it imports the net module.
It then creates a server. The server has a callback function which adds event listeners to the socket.
A socket is the term for the endpoint of a communication stream across a network. If it is bi-directional (this one is), it can send data down the stream both ways.
The socket is how we interface with the data that the server receives. We can add an event listener using the .on() function. When the socket receives data, it will call the callback function applied to the data event listener, which in this case prints to the console whatever data that it receives, then hashes the data and sends it back down the stream to the client. In actual use, it would pass the data on to be processed. It sends the data back using socket.write(hashed).
There is a second event listener that ends the socket when it detects an end, to make sure the socket on the sever side ends when the .
Finally, we call server.listen(), which starts the server listening on port 80. We give it the parameter 80, which is the HTTP port (to ensure traffic is not blocked). This means that the server will reply to all traffic on the network that is in port 80.
xxxxxxxxxx111var client = new net.Socket();2client.connect(80, "127.0.0.1", function() {3 client.write("Hash this string please")4 client.on("data", function(data) {5 console.log("Client received: " + data)6 client.destroy()7 });8 client.on("close", function() {9 console.log("Connection closed")10 })11})The client creates a socket which we connect to the same port as our server, and the IP 127.0.0.1, which is the loopback address, meaning it connects to itself without leaving the machine. When it connects, it sends to the server "hash this string please" using client.write(). Using an event listener once again, it prints to the console whatever data that it receives, then destroys the socket. It then uses another event listener to detect that event and print "Connection closed" to the console.
Running the program creates this output:

This simply model clearly demonstrates how we can use the net module in order to send data from client to client.
However, we need a system that determines what messages need to be sent. I made some flowcharts to map the logic that the program should follow:
When it connects, it will then go through this flowchart:
Then there are also the other message functions that the client can receive, which are:
The is a lot of data that we will need to store. The way that we do this is by storing it in the applications' %APPDATA% folder, which we can access using the remote module.
xxxxxxxxxx11var path = remote.app.getPath('appData')+'/arbitra-client/We can then store data using the fs (File System) module, which will be covered during the Technical Solution phase.
Files will be in JSON (JavaScript Object Notation), because as the name suggests it is directly compatible with
There are several different files that we need to have. The obvious ones are listed here:
| Name | Description | Type |
|---|---|---|
blockchain.json | The blockchain | {} |
connections.json | List of nodes that are currently connected | [] |
recent-connections.json | Nodes that have been connected to | [] |
sent.json | Sent messages so they aren't resent | [] |
network-setting.json | Settings options | {} |
error-log.json | Stores error messages | [] |
txpool.json | Pending transactions that are used to generate blocks | [] |
wallets.json | Stores wallets | [] |
The type indicates if the file will be in Array form or in Object literal form.
First, we need to work out some of the differences between Python and Javascript. First of all, and most importantly, Javascript only supports numbers up to , which is an issue considering , a significantly larger number. Whereas Python handles this automatically, we need to use a library to handle the large numbers that we have to use. I chose https://www.npmjs.com/package/big-integer, as it has a mod() function, which others that I looked at lacked.
Installing via npm:
xxxxxxxxxx71C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client>npm install --save big-integer2arbitra-client@0.1.1 C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client3`-- big-integer@1.6.2645npm WARN arbitra-client@0.1.1 No description6npm WARN arbitra-client@0.1.1 No repository field.7npm WARN arbitra-client@0.1.1 No license field.We then require() it at the top of the document.
xxxxxxxxxx11const bigInt = require('big-integer')Secondly, JS has no tuples. Therefore, we need to represent points as an object literal.
xxxxxxxxxx11var point = {x=20,y=21}We can then get the value by calling point.x to get 20. JS also has support for Infinity, which we can use as the point at infinity.
Thirdly, while JS does have classes, it would probably be better to just have the curve characteristics as a const and not have the functions in a class. The curve therefore looks like:
xxxxxxxxxx111// elliptic curve secp256k12const curve = {3 a: bigInt('0'),4 b: bigInt('7'),5 p: bigInt('115792089237316195423570985008687907853269984665640564039457584007908834671663'),6 g: {7 x: bigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),8 y: bigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424')9 },10 n: bigInt('115792089237316195423570985008687907852837564279074904382605163141518161494337')11}When we want , for example, we would call curve.p.
I put curve and sha256() into a new file called ecdsa.js, in the /js/ folder, which will contain all our ECDSA code.
Fourthly, we want to make sure that this code runs asynchronously. This means that that the rest of the client can do whatever it wants, while this signature code runs for as long as is wants without disrupting the rest of the program. To do this, we use callback functions. These work by instead of returning a value at the end of a function, we instead pass a different function to the the first function which it then passes values to.
Finally, we need random numbers
In Python, we used random.randrange to generate the private key and . However, there is no equivalent function in crypto. We could use crypto.randomBytes() to determine the secret key, as it claims to be cryptographically strong. However, since it needs to be less than , a naïve approach might be to % curve.n.
xxxxxxxxxx31var random = bigInt(crypto.randomBytes(256).toString(10))2// make sure it's less than n3var private = rand.mod(curve.n)However, values more than would wrap around to be small numbers again, which produces an uneven distribution of random numbers, which is not random. While there are modules to do this, we're going to have to simply generate random numbers until they are less than .
xxxxxxxxxx91function randomNum(min=1,max=curve.n) {2 var randomValue = max.add(1)3 while (randomValue.greater(max) || randomValue.lesser(min)) {4 // 32 bytes = 256 bits5 var buffer = crypto.randomBytes(32).toString('hex')6 randomValue = bigInt(buffer,16)7 }8 return randomValue9}Note this function can be passed minimum and maximum values, but default to and , as that is all we need the function for.
sha256()We need to convert the sha256() function we made earlier to return bigInts. This is relatively easy.
xxxxxxxxxx51function sha256(data) {2 // creates a sha256 hash, updates it with data, and turns it into a bigint3 var hash = crypto.createHash('sha256').update(data).digest('hex')4 return bigInt(hash,16)5}onCurve()This one is pretty simple to translate. However, we have to use big-integer, so that means using .minus() instead of simply - etc.
xxxxxxxxxx111function onCurve(point) {2 // sees if point is on the curve y^2 = x^3 + ax + b3 if (point !== Infinity) {4 ysq = point.y.square()5 xcu = point.x.pow(3)6 ax = curve.a.multiply(point.x)7 if (ysq.minus(xcu).minus(ax).minus(curve.b).isZero()) {8 throw new Error('not on curve')9 }10 }11}invMod()big-integer has an modInv() function which is identical, but is designed to work specifically with bigInts, so I decided to use that instead.
addPoints()Now we have the other functions, we can implement addPoints(). Due to big-integer, it looks incredibly messy, although I tried to separate out the expressions a bit
xxxxxxxxxx361function addPoints(P1,P2) {2 onCurve(P1)3 onCurve(P2)4 var m,x,y5 // Point + Infinity = Point6 if (P1 === Infinity) {7 return P28 } else if (P2 === Infinity) {9 return P110 }11 if (P1.x === P2.x) {12 if (P1.y !== P2.y) {13 return Infinity14 } else {15 // finding gradient of tangent16 var t1 = bigInt(3).times(P1.x.square())17 var t2 = bigInt(2).times(P1.y)18 m = bigInt(t1.plus(curve.a)).times(t2.modInv(curve.p))19 }20 } else {21 // finding gradient of line between 2 points22 var t1 = P2.y.minus(P1.y)23 var t2 = P2.x.minus(P1.x)24 m = t1.times(t2.modInv(curve.p))25 }26 // calculating other interception point27 x = bigInt(m.square().minus(P1.x).minus(P2.x)).mod(curve.p)28 y = bigInt(bigInt(m.times(P1.x)).minus(P1.y).minus(m.times(x))).mod(curve.p)29 var P3 = {30 x: x,31 y: y32 }33 onCurve(P3)34 return P335}36multiPoints()With multiPoints(), we need to convert a number to binary then iterate through it. First of all, we need to get the binary number. Luckily, when we use .toString(), we can specify the base, in this case .toString(2). Otherwise, the function is very similar to the Python version. yranib is the word "binary" reversed.
xxxxxxxxxx201function multiPoints(n, P) {2 if (P === Infinity) {3 return P4 }5 var total = Infinity6 var binary = n.toString(2)7 // reversed binary8 var yranib = binary.split('').reverse()9 // see documentation if confused, it's a bit mathsy10 // to explain in comments11 yranib.forEach(function(bit) {12 if (bit == 1) {13 total = addPoints(total, P)14 }15 P = addPoints(P, P)16 onCurve(P)17 })18 onCurve(total)19 return total20}createKeys()Now for the key creation function. Here is where we use randomNum().
xxxxxxxxxx61function createKeys() {2 var rand = randomNum(1,curve.n)3 var private = rand % curve.n4 var public = multiPoints(private,curve.g)5 return {private = private, public = public}6}However, createKeys() itself should be an asynchronous function, since createKeys() takes time to run and things rely on it. Therefore, I restructured it.
xxxxxxxxxx111function createKeys(callback) {2 var err3 try {4 var private = randomNum(1, curve.n)5 var public = multiPoints(private, curve.g)6 } catch (e) {7 err = e8 } finally {9 callback(public, private, err)10 }11}This is now much simpler and should work much better down the road.
signMsg()signMsg() also uses callbacks. It also catches any errors in the multiPoints() function, which would appear if w was an invalid number, and return a error to the callback. I also restructured the function around a while loop which is much easier to understand.
xxxxxxxxxx201function signMsg(msg,w,callback) {2 var err3 console.log("Signing: "+msg)4 var z = hash.sha256(msg)5 var r,s6 r = s = bigInt.zero7 while (r.isZero() && s.isZero()) {8 var k = randomNum(1, curve.n)9 try {10 var P = multiPoints(k,curve.g)11 } catch(e) {12 err = e13 callback(0,0,err)14 return15 }16 r = P.x.mod(curve.n)17 s = bigInt(bigInt(w.times(r).plus(z)).times(k.modInv(curve.n))).mod(curve.n)18 }19 callback(r,s,err)20}verifyMsg()verifyMsg() is also similar to it's Python equivalent
xxxxxxxxxx141function verifyMsg(msg,r,s,q,callback) {2 var result = false3 var z = hash.sha256(msg)4 u1 = bigInt(z.times(s.modInv(curve.n))).mod(curve.n)5 u2 = bigInt(r.times(s.modInv(curve.n))).mod(curve.n)6 try {7 P = addPoints(multiPoints(u1,curve.g),multiPoints(u2,q))8 } catch(e) {9 callback(result)10 return11 }12 result = bigInt(r.mod(curve.n)).equals(P.x.mod(curve.n))13 callback(result)14}Finally, we can export the functions that the rest of the program will interact with: createKeys(), signMsg(), and verifyMsg(), as well as curve in case we need to reference the curve attributes somewhere else in the program.
xxxxxxxxxx41exports.curve = curve2exports.createKeys = createKeys3exports.signMsg = signMsg4exports.verifyMsg = verifyMsgIn testing.js, I added the following to init():
xxxxxxxxxx131function init() {2 console.log("testing.js loaded")3 ecdsa.createKeys((public,private,err) => {4 if (err) {5 alert("Error: "+err)6 throw err7 } else {8 console.log(private.toString(16))9 console.log(public.x.toString(16))10 console.log(public.y.toString(16))11 }12 })13}This returns:
xxxxxxxxxx51renderer.js:13 Page change: testing2C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:5 testing.js loaded3C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:11 64287b362b58ca40853292540e93d6233426d7113dd3e8eac7e4fa3bbf587c274C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:12 dc07c1ba078ad9cd4231ed0d5b75a8cf2f7db601e665fe25e425d475b07372265C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:13 155b51fdbb9207cbf6dc9904a6056412ccd8c62fd2bfbdecf1ba6e8942af65086This appeared to work. We now need to test the message signing/verification to confirm that. I changed init() to this:
xxxxxxxxxx251function init() {2 console.log("testing.js loaded")3 ecdsa.createKeys((public,private,err) => {4 if (err) {5 alert("Error: "+err)6 throw err7 } else {8 console.log(private.toString(16))9 console.log(public.x.toString(16))10 console.log(public.y.toString(16))11 ecdsa.signMsg("bean",private,(r,s,err) => {12 if (err) {13 alert("Error: "+err)14 throw err15 } else {16 console.log(r.toString(16))17 console.log(s.toString(16))18 ecdsa.verifyMsg("bean",r,s,public,(result) => {19 console.log(result)20 })21 }22 })23 }24 })25}This creates keys, signs the message "bean", then verifies the message, throwing any errors it picks up along the way. This returns:
xxxxxxxxxx91renderer.js:13 Page change: testing2C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:5 testing.js loaded3C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:11 2648d426958d128260e3b76934a54287119acb24d00f371d91fc354406cf7ac4C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:12 fff20f5fd4128e6f21cf5e344f9879bd92533e9e2c30ab841ce6732425d9fa985C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:13 -5a430f49825fb7fde918327ed5d9618778ff9d48c5ab18277a2273695d18d9236C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\ecdsa.js:110 Signing: bean7C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:19 998af03e55c252ee98144208c3e06e0c788065ad6a39f170943d5d577acddb48C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:20 30f855da1ec7fe114e52ab4a0f9373893ded2801d151f5152daf4462a08af6dc9C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\testing.js:22 trueThis means that it works. We can now use ecdsa.js to sign and verify messages.
While the mathematical aspect of the function works, it is inconvenient to call from other parts of the program, since all the keys are in bigInt format within ecdsa.js, and are as hexadecimal strings everywhere else. Therefore, we need to change these functions so that they take in and return values that are compatible with the rest of the program. I started with createKeys().
xxxxxxxxxx151function createKeys(callback) {2 var err3 try {4 var private = randomNum(1, curve.n)5 var public = multiPoints(private, curve.g)6 var x = public.x.toString(16)7 var y = public.y.toString(16)8 public = x+y9 private = private.toString(16)10 } catch (e) {11 err = e12 } finally {13 callback(public, private, err)14 }15}Now it returns hexadecimal strings, so the rest of the program can take that output and use it without conversion.
Next we need to convert signMsg(). This is relatively simple, as it only took one bigInt as an argument. However, we also need to turn the signature into one hexadecimal string.
xxxxxxxxxx41function signMsg(msg,private,callback) {2 var err3 var w = bigInt(private,16)4 Finally, the last outward-facing function is verifyMsg(). This is more complex, as we need to parse the signature,
We have already converted the sha256() function from Python in the ECDSA section of this document. However, since it is an important function, I put it in it's own file, hashing.js, which is located in the /js/ directory. I then created two functions, one which returns a bigInt, and another which returns a hexadecimal string, and also set up the exports function.
xxxxxxxxxx151const crypto = require('crypto')2const bigInt = require('big-integer')34function sha256(data) {5 // creates a sha256 hash, updates it with data, and turns it into a bigint6 var hash = crypto.createHash('sha256').update(data).digest('hex')7 return bigInt(hash,16)8}910function sha256hex(data) {11 return sha256(data).toString(16)12}1314exports.sha256 = sha25615exports.sha256hex = sha256hexNow we can import our hashing function from all over the application.
As mentioned previously, to avoid messages going in circles we need to store the hashes of received messages so that we don't resend it to people. For this, we will store in a JSON file the hash of the sent messages, along with the IP addresses that it has sent them to. The file will be in the following format:
xxxxxxxxxx41{2 "b3fa55f98fcfcaf6a15a7c4eb7cdd1b593693d3fef2fb7aec3b6768fd7c6a4ce": ["168.12.143.1","168.991.125.6"],3 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824": ["127.0.0.1"]4}By using the hash as a key, we can simply use file[hash] to retrieve the array of addresses that that message was sent to, which is far easier and more efficient than having to search for it.
The method by which you read and write to files in Node.js is using the fs (file system) module, a default library. However, it is powerful but quite clumsy, so I created some functions that wrap around fs' functions that are better adapted to this project's needs. These are stored in file.js.
store()First we need to import fs and electron.remote into file.js, since we need remote to get the file path of the %APPDATA% folder where the JSON files will be located.
xxxxxxxxxx21const remote = require('electron').remote2const fs = require('fs')Then we make a new function called store(). This will open the JSON file, append a new hash, and then save it. We get the correct file path using remote.app.getPath('appData'). I had originally designed this function to just work for sent.json, but this is changed later.
xxxxxxxxxx51function store(key,data) {2 // put data in file3 var path = remote.app.getPath('appData')4 fs.readFile(path+'sent.json','utf-8',(err,contents) => {}) 5}Then we need to get the contents and JSON.parse() it, then add our new data.
xxxxxxxxxx101function store(key,data) {2 // put data in file3 var path = remote.app.getPath('appData')+'sent.json'4 fs.readFile(path,'utf-8',(err,content) => {5 if (err) throw err6 var jsondata = JSON.parse(content)7 jsondata.push(data)8 content = JSON.stringify(jsondata)9 })10}We then rewrite content to the file.
xxxxxxxxxx161function store(key,data) {2 // put data in file3 var path = remote.app.getPath('appData')+'\\arbitra-client\\sent.json'4 fs.readFile(path,'utf-8',(err,content) => {5 if (err) {6 throw err7 } else {8 var jsondata = JSON.parse(content)9 jsondata[key] = data10 content = JSON.stringify(jsondata)11 fs.writeFile(path,content,'utf-8',(err) => {12 if (err) throw err13 })14 }15 })16}However, this will throw an error if sent.json doesn't exist. We can fix this by catching the corresponding error (ENOENT) and creating a new file if that's the case. We do however have to put content into an array so it is in the correct format.
xxxxxxxxxx261function store(key,data) {2 // put data in file3 var path = remote.app.getPath('appData')+'\\arbitra-client\\sent.json'4 fs.readFile(path,'utf-8',(err,content) => {5 if (err) {6 if (err.code === 'ENOENT') {7 console.log('Creating sent.json')8 var content = {key:data}9 content = JSON.stringify(content)10 fs.writeFile(path,content,'utf-8',(error) => {11 if (error) throw error12 })13 } else {14 console.log(err.code)15 throw err16 }17 } else {18 var jsondata = JSON.parse(content)19 jsondata.push(data)20 content = JSON.stringify(jsondata)21 fs.writeFile(path,content,'utf-8',(err) => {22 if (err) throw err23 })24 }25 })26}However, this doesn't quite work, when we call the function with the following input:
xxxxxxxxxx31var key = 'b3fa55f98fcfcaf6a15a7c4eb7cdd1b593693d3fef2fb7aec3b6768fd7c6a4ce'2var data = ['168.12.143.1','168.991.125.6']3store(key,data)It creates sent.json, as expected...
xxxxxxxxxx31C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\file.js:10 Creating sent.json2renderer.js:13 Page change: overview3C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\overview.js:2 overview.js loadedHowever, the actual contents of sent .json looks like this:
xxxxxxxxxx11[object Object]This is an easy fix, as I simply forgot to JSON.stringify(content) when creating the file. However, it does raise the issue about how to deal with invalid data when opening sent.json. I decided restructure the function so that it would deal with unexpected errors better.
xxxxxxxxxx301function store(key,data) {2 // put data in file3 var path = remote.app.getPath('appData')+'\\arbitra-client\\sent.json'4 fs.readFile(path,'utf-8',(err,content) => {5 if (err) {6 // if the file doesn't exist, it sets content to an array7 // it will then continue on and create the file later8 if (err.code === 'ENOENT') {9 content = '{}'10 } else {11 alert('Error opening sent.json')12 throw err13 }14 }15 // try to parse content to js then push the data16 try {17 var jsondata = JSON.parse(content)18 jsondata[key] = data19 } catch(e) {20 console.warn(e)21 var jsondata = {key:data}22 }23 // writes the contents back to the file24 // or makes the file if it doesn't exist yet25 content = JSON.stringify(jsondata)26 fs.writeFile(path,content,'utf-8',(err) => {27 if (err) throw err28 })29 })30}Now, the function opens sent.json, checks if there's an error, and if there's not it tries to parse the content. If it doesn't work, it uses an empty object. Finally, it puts the data into the object, turns it back into a string and writes this new string back into the file. If there is an error when reading the file, it checks to see if the error corresponds to the file not existing - if that's the case, it sets the content to '{}', an empty object. Otherwise, it will throw an error.
It was at this point I realised that we will want to use this in other situations as well, such as to store wallets or the blockchain. I therefore made it take a third argument, file.
xxxxxxxxxx371const remote = require('electron').remote2const fs = require('fs')34function store(key,data,file) {5 // put data in file6 // no callbacks because it's a subroutine7 var path = remote.app.getPath('appData')+'\\arbitra-client\\'+file+'.json'8 fs.readFile(path,'utf-8',(err,content) => {9 if (err) {10 // if the file doesn't exist, it creates an empty object literal11 // it will then continue on and create the file later12 if (err.code === 'ENOENT') {13 content = '{}'14 } else {15 alert('Error opening '+file+'.json')16 throw err17 }18 }19 // try to parse content to js then push the data20 try {21 var jsondata = JSON.parse(content)22 jsondata[key] = data23 } catch(e) {24 console.warn(e)25 var jsondata = {key:data}26 } finally {27 // writes the contents back to the file28 // or makes the file if it doesn't exist yet29 content = JSON.stringify(jsondata)30 fs.writeFile(path,content,'utf-8',(err) => {31 if (err) throw err32 })33 }34 })35}3637exports.store = storeFinally, we need to consider what happens when there the key already exists. We need to add the new data to the old data. To do this, we need to merge to two arrays. To do this, I found the best method was to first concatenate the arrays, then them into a set turn it into a Set then back into Javascript. We turn them into a Set in order to remove duplicates, as a Set will only accept unique values.
https://gist.github.com/telekosmos/3b62a31a5c43f40849bb#gistcomment-1826809
xxxxxxxxxx241 // try to parse content to js then push the data2 try {3 var jsondata = JSON.parse(content)4 if (jsondata.hasOwnProperty(key)) {5 // if the key exists it concatenates the two arrays, creates a new set6 // which removes duplicates, then turns it back to an array7 // https://gist.github.com/telekosmos/3b62a31a5c43f40849bb#gistcomment-18268098 var set = new Set(jsondata[key].concat(data))9 jsondata[key] = Array.from(set)10 } else {11 // otherwise sets the key to the data12 jsondata[key] = data13 }14 } catch(e) {15 console.warn(e)16 var jsondata = {key:data}17 } finally {18 // writes the contents back to the file19 // or makes the file if it doesn't exist yet20 content = JSON.stringify(jsondata)21 fs.writeFile(path,content,'utf-8',(err) => {22 if (err) throw err23 })24 }If we want to make it truly generic, we need to get rid of the code relating to arrays, or at least put it in a separate function. I decided to solve this by adding a check to see what type data is. If it's an array, we do the concatenation thing with Set, otherwise we just replace it. I originally tried to do this using typeof, however upon testing, it simply returns "object" for arrays, which is a problem given that we want to differentiate between array objects and other objects.
xxxxxxxxxx21typeof ['hi']2"object"However, I found out that we can simply use Array.isArray(), which returns a simple Boolean. I put this in the if statement with jsondata.hasOwnProperty(key), so that it only does the concatenation thing if both the key exists and the data is an array.
xxxxxxxxxx101if (jsondata.hasOwnProperty(key) && Array.isArray(data)) {2 // if the key exists it concatenates the two arrays, creates a new set3 // which removes duplicates, then turns it back to an array4 // https://gist.github.com/telekosmos/3b62a31a5c43f40849bb#gistcomment-18268095 var set = new Set(jsondata[key].concat(data))6 jsondata[key] = Array.from(set)7} else {8 // otherwise sets the key to the data9 jsondata[key] = data10}Finally, I added an optional callback that is called when the file is written to. I did this by giving callback the default value of ()=>{}, which is ES6 arrow notation for a function, meaning that if no argument is passed for callback an empty function is called instead.
xxxxxxxxxx421function store(key,data,file,callback=()=>{}) {2 // put data in file3 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'4 fs.readFile(path,'utf-8',(err,content) => {5 if (err) {6 // if the file doesn't exist, it creates an empty object literal7 // it will then continue on and create the file later8 if (err.code === 'ENOENT') {9 content = '{}'10 } else {11 alert('Error opening '+file+'.json')12 throw err13 }14 }15 // try to parse content to js then push the data16 try {17 var jsondata = JSON.parse(content)18 if (jsondata.hasOwnProperty(key) && Array.isArray(data)) {19 // if the key exists it concatenates the two arrays, creates a new set20 // which removes duplicates, then turns it back to an array21 // https://gist.github.com/telekosmos/3b62a31a5c43f40849bb#gistcomment-182680922 var set = new Set(jsondata[key].concat(data))23 jsondata[key] = Array.from(set)24 } else {25 // otherwise sets the key to the data26 jsondata[key] = data27 }28 } catch(e) {29 console.warn(e)30 var jsondata = {}31 jsondata[key] = data32 } finally {33 // writes the contents back to the file34 // or makes the file if it doesn't exist yet35 content = JSON.stringify(jsondata)36 fs.writeFile(path,content,'utf-8',(err) => {37 if (err) throw err38 callback()39 })40 }41 })42}I tested this by calling store('test',['data1','data2'],'test'). This returned:
xxxxxxxxxx11{"test":["data1","data2"]}In the file C:\Users\Mozzi\AppData\Roaming\arbitra-client\test.json.
Next I called store('test2','hello','test').
xxxxxxxxxx11{"key":["data1","data2"],"test2":"hello"}Strangely, the key 'test' has been replaced by 'key'. I guessed that it was an error relating to using {key:data} if there was an exception, so I replaced all instances of that with:
xxxxxxxxxx21var jsondata = {}2jsondata[key] = dataand ran the same tests again. This ended up with:
xxxxxxxxxx11{"test":["data1","data2"]}and then:
xxxxxxxxxx11{"test":["data1","data2"],"test2":"hello"}We can now test the array concatenation. Calling store('test',['data2','data3'],'test') gave:
xxxxxxxxxx11{"test":["data1","data2","data3"],"test2":"hello"}It worked!
get()Now we need to retrieve data. This function is very similar, except instead of writing to the file at the end, it just calls the callback with the data it retrieved.
It also returns null if either the file or the key doesn't exist, which can be dealt with in the callback.
xxxxxxxxxx271function get(key,file,callback) {2 var path = remote.app.getPath('appData')+'\\arbitra-client\\'+file+'.json'3 fs.readFile(path,'utf-8',(err,content) => {4 if (err) {5 // if the file doesn't exist, return null6 if (err.code === 'ENOENT') {7 callback(null)8 return9 } else {10 alert('Error opening '+file+'.json')11 throw err12 }13 }14 // try to parse content to js then push the data15 try {16 var jsondata = JSON.parse(content)17 var result = jsondata[key]18 } catch(e) {19 // if the key doesn't exist, return null20 console.warn(e)21 var result = null22 } finally {23 callback(result)24 return25 }26 })27}However, I noticed later on that returning null if no data is found was awkward to work around. I therefore added fail, which is an optional parameter which is returned if no data is found.
xxxxxxxxxx281function get(key,file,callback,fail=null) {2 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'3 fs.readFile(path,'utf-8',(err,content) => {4 if (err) {5 // if the file doesn't exist, return null6 if (err.code === 'ENOENT') {7 console.warn(file+'.json not found')8 console.trace()9 callback(fail)10 return11 } else {12 alert('Error opening '+file+'.json')13 throw err14 }15 }16 // try to parse content to js then push the data17 try {18 var jsondata = JSON.parse(content)19 var result = jsondata[key]20 } catch(e) {21 // if the key doesn't exist, return null22 console.warn(e)23 var result = fail24 } finally {25 callback(result)26 }27 })28}I also now exported the functions, so they can be used in other files.
xxxxxxxxxx71const remote = require('electron').remote2const fs = require('fs')34// functions go here56exports.store = store7exports.get = getI realised that it would be helpful to add some more functions to deal with other problems. These ended up being:
getAll(), a wrapper around fs.readFile()storeAll(), the corresponding wrapper around fs.writeFile()append(), which appends data to a file which contains an array rather than an object literal.getAll()This is simply stripping the complicated parts out of get().
xxxxxxxxxx201function getAll(file,callback,fail=null) {2 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'3 fs.readFile(path,'utf-8',(err,content) => {4 if (err) {5 // if the file doesn't exist, return null6 if (err.code === 'ENOENT') {7 console.warn(file+'.json not found')8 content = fail9 } else {10 alert('Error opening '+file+'.json')11 console.error('Error opening '+file+'.json')12 throw err13 }14 }15 callback(content)16 })17}1819// this is at the bottom of the file with the others20exports.getAll = getAllstoreAll()storeAll() is even simpler, as it just wraps fs.writeFile().
xxxxxxxxxx81function storeAll(file,data,callback=()=>{}) {2 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'3 content = JSON.stringify(data)4 fs.writeFile(path,content,'utf-8',(err) => {5 if (err) throw err6 callback()7 })8}append()Append is very similar to get(), but rather than dealing with objects that sometimes have arrays as data and using Array.isArray(), it simply appends the data to the file using jsondata.push() as the file is assumed to be an array rather than an object literal.
xxxxxxxxxx321function append(file,data,callback=()=>{}) {2 // write data to a file, but where the file is an array so no key3 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'4 fs.readFile(path,'utf-8',(err,content) => {5 if (err) {6 // if the file doesn't exist, it creates an empty object literal7 // it will then continue on and create the file later8 if (err.code === 'ENOENT') {9 content = '[]'10 } else {11 alert('Error opening '+file+'.json')12 throw err13 }14 }15 // try to parse content to js then push the data16 try {17 var jsondata = JSON.parse(content)18 jsondata.push(data)19 } catch(e) {20 console.warn(e)21 var jsondata = [data]22 } finally {23 // writes the contents back to the file24 // or makes the file if it doesn't exist yet25 content = JSON.stringify(jsondata)26 fs.writeFile(path,content,'utf-8',(err) => {27 if (err) throw err28 callback()29 })30 }31 })32}In the design phase we figured out how to use the net module to set up a TCP server. Now, we need to create a system where we can send and receive messages in the background of the application, all while the rest of the app does it's own stuff. First of all, I put the code from the design stage in it's own file, network.js. We can them import it into renderer.js to run in the background of the application.
xxxxxxxxxx331const net = require('net')2const hash = require('./hashing.js')34function init() {5 var server = net.createServer((socket) => {6 console.log("Server created")7 socket.on("data",(data) => {8 console.log("Server received: "+data)9 var hashed = hash.sha256hex(data)10 socket.write(hashed)11 })12 socket.on("end",socket.end)13 })14 15 server.listen(2018)16}1718function sendMsg(message,ip) {19 var client = new net.Socket()20 client.connect(2018,ip,() => {21 client.write(message)22 client.on("data",(data) => {23 console.log("Client received: "+data)24 client.destroy()25 })26 client.on("close",() => {27 console.log("Connection closed")28 })29 })30}3132exports.init = init33exports.sendMsg = sendMsgThen in renderer.js, after it calls the changePage() function, I put this:
xxxxxxxxxx61const network = require('./js/network.js')2345network.init()6network.sendMsg("hello","127.0.0.1")This produced:
xxxxxxxxxx61C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\network.js:6 Server created2C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\network.js:8 Server received: hello3C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\network.js:23 Client received: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b98244C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\network.js:27 Connection closed5renderer.js:13 Page change: overview6C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\js\overview.js:2 overview.js loadedThis is promising. We can run the sendMsg() function from anywhere and the server will reply. Now we need to structure the server so that it can reply as we want it to.
xxxxxxxxxx101var server = net.createServer((socket) => {2 console.log("Server created")3 socket.on("data",(data) => {4 console.log("Server received: "+data)5 parseMsg(data,(reply) => {6 socket.write(reply)7 })8 })9 socket.on("end",socket.end)10 })This passes received data to the parseMsg() function, which should return a reply which the server then sends back. This will be created later. For testing purposes, it simply returns a hash of the input message.
xxxxxxxxxx31function parseMsg(data,callback) {2 callback(hash.sha256hex(data))3}Since we'd never be sending one message at a time, I created a function that opens connections.json and sends a message to each of the IPs within, sendToAll(). It uses a function file.getAll() which I haven't created yet, but in summary it opens the file with the name passed to it, and returns the data in that file. This function calls file.getAll() to get connections.json, then iterates through the objects in the file using forEach(), and sends a message to each of them.
xxxxxxxxxx121function sendToAll(msg) {2 file.getAll('connections',(data) => {3 // doesn't do anything if there's no connections4 if (data !== null || data === '' || data === '[]') {5 nodes = JSON.parse(data)6 // go through connections and send a message to each7 nodes.forEach((node) => {8 sendMsg(msg,node.ip)9 })10 }11 })12}But what causes clients to start sending messages in the first place? We need to make the system where it figures out the data it needs and starts asking for it without prompting. When the app starts up, it needs to do the following:
I decided the best place for this is within the network.init() function, after we set the server running. First of all, we need to wipe connections.json, as this is meant to contain current connections and it is unknown if those connections are still there. Therefore, we remove it's contents when the application starts. This uses file.storeAll(), which stores an empty array.
xxxxxxxxxx31 // wipe connections2 // this will be populated with connections that succeed3 file.storeAll('connections',[])Next, we need to connect to the nodes listed in recent-connections.json, to reaffirm that they are still around. I decided to put this in a function called connect().
connect()This is a fairly simple function, as it simply gets all the nodes in recent-connections.json and sends them a ping, if they're not currently connected to. However, I also wanted it to connect to the backup server if it couldn't find any connections. Therefore, after it sends the pings I wanted to see how many connections there are, and if there's none it should attempt to connect to the backup server. First, I ran into the issue of knowing how many connections I had. Initially, I tried a complex callback system but this ultimately failed.
I realised later that I could just use the connection counter in the top left corner. This is increased every time the client receives a ping, so if we read that we can tell how many connections the client has.
The second issue is that pings don't come back instantly. Therefore, I used the setTimeout() function to check the number of connections after 10000 milliseconds have passed.
Finally, the backup server is 5.81.186.90. I also show the "WARNING: No connections" message until the ping has returned.
xxxxxxxxxx431function connect() {2 // try to connect to other nodes through old connections3 file.getAll('recent-connections',(data) => {4 var connections = JSON.parse(data)5 file.get('advertise','network-settings',(data) => {6 if (data === 'true' || data === 'false') {7 var advertise = data8 } else {9 var advertise = 'true'10 }11 var ping = {12 "header": {13 "type": "pg"14 },15 "body": {16 "advertise": advertise17 }18 }19 file.getAll('connections',(curdata) => {20 var current = JSON.parse(curdata)21 connections.forEach((node) => {22 if (!current.includes(node)) {23 sendMsg(ping,node.ip)24 }25 })26 },'[]')27 28 // wait ten seconds to see if any connections have been made29 setTimeout(() => {30 // get the number of connections from textContent31 var connectCount = parseInt(document.getElementById('connections').textContent)32 if (connectCount === 0) {33 console.warn('No connections found!')34 document.getElementById('nonodes').classList.remove('hidden')35 console.warn('Connecting to backup server')36 // wavecalcs.com is friend's server, and should be online for the purposes of this project37 // wavecalcs.com = 5.81.186.9038 sendMsg(ping,'5.81.186.90')39 }40 },10000)41 })42 },'[]')43}setInterval()As per the list, the next thing we need to do is create a loop that sends out hash request messages and pings, where neccessary. Rather than use a while loop, I found that a better way of doing it was by using the setInterval() function, as then we can set a gap between the loops. I chose 60 seconds, or 60000 milliseconds.
What this function does it get the number of connections from the DOM, as mentioned previously, and also get the target number of connections from the settings file. Then, if the number of connections is less than the number of target connections, it calls the connect function again. It then waits 15 seconds for that function to finish, then gets the number of connections again to see if it's over the threshold yet. If not, it sends out node requests to the connections in an attempt to get more nodes.
xxxxxxxxxx281 // this is a loop that maintains connections and2 // sends top hash requests to make sure the client is up to date3 // it goes on forever, every minute4 setInterval(() => {5 console.log('Interval')6 var connections = parseInt(document.getElementById('connections').textContent)7 // first check that we have enough connections8 file.get('target-connections','network-settings',(target) => {9 // if the current number of of connections is less than the minimum10 // as defined by user settings, connect11 if (connections < target) {12 connect()13 // if it's still not enough after 15 seconds, send node requests14 setTimeout(() => {15 connections = parseInt(document.getElementById('connections').textContent)16 if (connections < target) {17 var nr = {18 "header": {19 "type": "nr"20 },21 "body": {}22 }23 sendToAll(nr)24 }25 },15000)26 }27 },5) // if it fails to open the file it sets target to five28 },60000)I also wanted it to send out hash requests, so I added that. On top of that, if connections is greater than the target, it needs to replace recent-connections.json with connections.json.
It only sends out hash requests if there are more than one connections. If there are no connections, it removes the .hidden classs from the "no connections" warning.
xxxxxxxxxx481// this is a loop that maintains connections and2// sends top hash requests to make sure the client is up to date3// it goes on forever, every minute4setInterval(() => {5 console.log('Interval')6 var connections = parseInt(document.getElementById('connections').textContent)7 // first check that we have enough connections8 file.get('target-connections','network-settings',(target) => {9 // if the current number of of connections is less than the minimum10 // as defined by user settings, connect11 if (connections < target) {12 connect()13 // if it's still not enough after 15 seconds, send node requests14 setTimeout(() => {15 connections = parseInt(document.getElementById('connections').textContent)16 if (connections < target) {17 var nr = {18 "header": {19 "type": "nr"20 },21 "body": {}22 }23 sendToAll(nr)24 }25 },15000)26 } else {27 // save current connections to recent connections28 file.getAll('connections',(data) => {29 if (connections !== null) {30 file.storeAll('recent-connections',JSON.parse(data))31 }32 })33 }34 connections = parseInt(document.getElementById('connections').textContent)35 if (connections === 0) {36 document.getElementById('nonodes').classList.remove('hidden')37 } else {38 // check that the chain is up to date39 var hr = {40 "header": {41 "type": "hr"42 },43 "body": {}44 }45 sendToAll(hr)46 }47 },5) // if it fails to open the file it sets target to five48},60000)Once a message has been received, we need to parse it and send it's information on into the system. We also need to ensure that a message is valid, as one of the critical parts of a cryptocurrency is ensuring that all messages are correct and valid.
First, I created an example message, that is a transaction.
xxxxxxxxxx151var message = {2 header: {3 type: "tx",4 hash: "b3fa55f98fcfcaf6a15a7c4eb7cdd1b593693d3fef2fb7aec3b6768fd7c6a4ce",5 size: 106 version: "0.0.1"7 time: 129318028 },9 body: {10 sender: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",11 reciever: "b3fa55f98fcfcaf6a15a7c4eb7cdd1b593693d3fef2fb7aec3b6768fd7c6a4ce",12 amount: "12",13 time: 151683187981614 }15}This is a JSON object that represents a transaction. Whilst it is not at all correct, we can use it for testing the system as we build it. The function that the server calls is called parseMsg(), and it takes the received data and a callback function. First of all, we need to be able to store sent transactions.
parseMsg()The best way of doing this that I can think of is having a function for each message type. First, we need to create the if statement that handles this. First, it creates an object literal for the reply. It then attempts to parse the message into JSON. If it doesn't parse, it catches the error and calls the er() function, which takes the error message, which should set the reply to an error message. Otherwise, it checks the header type and calls the corresponding function. After the reply has been constructed, the header is created and then the reply is turned back to a string. Finally, it is passed to the callback function where it will be sent back to the sender.
xxxxxxxxxx331function parseMsg(data,callback) {2 var reply3 try {4 var msg = JSON.parse(data)5 if (msg.header.type === 'tx') {6 reply = tx(msg)7 } else if (msg.header.type === 'bk') {8 reply = bk(msg)9 } else if (msg.header.type === 'hr') {10 reply = hr(msg)11 } else if (msg.header.type === 'br') {12 reply = br(msg)13 } else if (msg.header.type === 'pg') {14 reply = pg(msg,ip)15 } else if (msg.header.type === 'nr') {16 reply = nr(msg,ip)17 } else {18 reply = er('type')19 }20 } catch(e) {21 if (e.name === 'SyntaxError') {22 console.warn(e)23 er('parse')24 } else {25 throw e26 }27 } finally {28 reply.header.time = Date.now()29 reply.header.hash = hash.sha256hex(JSON.stringify(reply.body))30 var replystr = JSON.stringify(reply)31 callback(replystr)32 }33}First however, we can simplify this system slightly. Every message has a hash, so we can verify that once before we start parsing the messages. Therefore, I added the check before the if statements. If it fails, it will call er() with the error message of 'hash'.
xxxxxxxxxx141try {2 var msg = JSON.parse(data)3 if (msg.header.hash === hash.sha256hex(JSON.stringify(msg.body))) {4 if (msg.header.type === 'tx') {5 tx(msg)6 } else if (msg.header.type === 'bk') {7 8 } else {9 er('type')10 }11 } else {12 er('hash')13 }14}I also realised that we could use the try...catch statement to set the error reply, rather than using er(). If it runs into an error, it simply throws the error, which will then be caught by the catch statement. By throwing what we want the return message to say, we can simply rewrite the try...catch statement like so:
xxxxxxxxxx221try {2 var msg = JSON.parse(data)3 if (msg.header.hash === hash.sha256hex(JSON.stringify(msg.body))) {4 if (msg.header.type === 'tx') {5 tx(msg)6 } else if (msg.header.type === 'bk') {7 8 } else {9 throw 'type'10 }11 } else {12 throw 'hash'13 }14} catch(e) {15 console.warn(e)16 reply.header.type = 'er'17 if (e.name === 'SyntaxError') {18 reply.body.err = 'parse'19 } else {20 reply.body.err = e21 }22}We also need to find the size of the body. I found an answer on StackOverflow that answered this question, using Node.js' Buffer - Buffer.byteLength(string,'utf8'). We put this at the end of the function.
xxxxxxxxxx71finally {2 reply.body.time = Date.now()3 reply.header.hash = hash.sha256hex(JSON.stringify(reply.body))4 reply.header.size = Buffer.byteLength(JSON.stringify(reply.body,'utf8'))5 var replystr = JSON.stringify(reply)6 callback(replystr)7}To make things a bit clearer, I decided to split these functions into a new file, parse.js, so that parseMsg() is more readable. However, this means that each of the functions like tx() will be converted to parse.tx().
I imported the following files, as all are needed.
xxxxxxxxxx51const network = require('./network.js')2const hash = require('./hashing.js')3const ecdsa = require('./ecdsa.js')4const blockchain = require('./blockchain.js')5const file = require('./file.js')The first of these functions is pg(). Since we need the IP of the node that sent it, we pass the IP as well as the message.
pg()What we need to achieve with this function is:
connections.json, the file which contains active connections.However, since we need to do the first part again when we receive a ping as a reply, I decided to split it up into two functions, one which adds it to connections.json, and the other which replies with the ping. pgreply() adds the IP to connections.json, and pg() calls pgreply() and then returns the reply message.
First, I created pgreply(). First, it creates the object which it will store later on. Then, it gets all the connections using the getAll() function I made. It then iterates through the connections, and sets repeat to true if the IP that sent the message is already in connections. If repeat is false, meaning that it is not already connected to that node, then it appends the store object, which contains the IP and whether or not the node wants to be advertised.
xxxxxxxxxx211function pgreply(msg,ip) {2 var store = {}3 store['ip'] = ip4 store['advertise'] = msg.body.advertise5 file.getAll('connections',(data) => {6 var repeat = false7 // checks to see if the ip is already in connections.json8 nodes = JSON.parse(data)9 nodes.forEach((node) => {10 if (node.ip === ip) {11 repeat = true12 }13 })14 // stores it if not15 if (!repeat) {16 file.append('connections',store,() => {17 console.log('Connection added: '+ip)18 })19 }20 },'[]')21}However, since we don't want to connect to ourselves, I decided to install the ip module and check if our IP is the same as the one we would be adding.
xxxxxxxxxx11>npm install --save ipWe then check if it's the same
xxxxxxxxxx71 // stores it if not and if it is not our ip2 const ourip = require('ip').address3 if (!repeat && ip !== ourip()) {4 file.append('connections',store,() => {5 console.log('Connection added: '+ip)6 })7 }Back to pg(). I created a new file in %APPDATA% called network-settings.json, and then added the following:
xxxxxxxxxx11{"advertise":"true"}This will be automatically added later on.
We then get() this file, then set the reply message accordingly. It checks to see if data is either 'true' or 'false', as I was having issues with that at an early stage.
xxxxxxxxxx211function pg(msg,ip,callback) {2 // store the connection3 pgreply(msg,ip)4 // send a reply5 var reply = {6 "header": {7 "type": "pg"8 },9 "body": {10 "advertise": advertise11 }12 }13 file.get('advertise','network-settings',(data) => {14 if (data === 'true' || data === 'false') {15 reply.body['advertise'] = data16 } else {17 reply.body['advertise'] = 'true'18 }19 })20 return reply21}I decided to test the ping function by creating function in testing.html to ping an IP. testing.html looks like this:
xxxxxxxxxx41<h1>Testing Page</h1>23<input type="text" id="sendto"/>4<button id="send">Send ping</button>testing.js is changed to this:
xxxxxxxxxx171const network = require('./network.js')23function init() {4 var msg = {5 "header": {6 "type": "pg",7 },8 "body": {9 "advertise": true10 }11 }12 document.getElementById('send').addEventListener('click',() => {13 network.sendMsg(msg,document.getElementById('sendto').value)14 })15}1617exports.init = initIt creates a message, msg, then when the send button is clicked it sends msg to the value of the input box using network.sendMsg().
Hopefully, if we send this ping to localhost we should see it print the same message twice, as the reply should be the same as what we sent.
xxxxxxxxxx71C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\network.js:169 Sending message: {"header":{"type":"pg","version":"0.0.1","size":39,"hash":"e925d7387f58e2a27ea686946400f180e122c2b68094418241ef857ea74af151"},"body":{"advertise":true,"time":1520933329222}}2C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\network.js:19 Received connection from: 127.0.0.13C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\network.js:20 Server received: {"header":{"type":"pg","version":"0.0.1","size":39,"hash":"e925d7387f58e2a27ea686946400f180e122c2b68094418241ef857ea74af151"},"body":{"advertise":true,"time":1520933329222}}4C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\network.js:29 Sending message to 127.0.0.1: {"header":{"type":"pg","version":"0.0.1","size":39,"hash":"e0e72e4798fc24ccfc4c31178488ad7e801a80b02294024c1ae868c089be77d"},"body":{"time":1520933329271}}5C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\network.js:178 Client received: {"header":{"type":"pg","version":"0.0.1","size":39,"hash":"e0e72e4798fc24ccfc4c31178488ad7e801a80b02294024c1ae868c089be77d"},"body":{"time":1520933329271}}6C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\parse.js:194 Connection added: 127.0.0.17C:\Users\Mozzi\Documents\Programming\arbitra\arbitra-client\parse.js:194 Connection added: localhostThis looks mostly good. However, I noticed that in the reply, it doesn't reply with a value for advertise, which is a problem. Looking again at the code for pg():
xxxxxxxxxx211function pg(msg,ip,callback) {2 // store the connection3 pgreply(msg,ip)4 // send a reply5 var reply = {6 "header": {7 "type": "pg"8 },9 "body": {10 "advertise": advertise11 }12 }13 file.get('advertise','network-settings',(data) => {14 if (data === 'true' || data === 'false') {15 reply.body['advertise'] = data16 } else {17 reply.body['advertise'] = 'true'18 }19 })20 return reply21}The issue lies in file.get() - since the data is returned in a callback, and since callbacks are asyncronous, reply.body.advertise is being set to data after the reply is returned. My first solution was to move the return reply into the callback.
xxxxxxxxxx211function pg(msg,ip,callback) {2 // store the connection3 pgreply(msg,ip)4 // send a reply5 file.get('advertise','network-settings',(data) => {6 if (data === 'true' || data === 'false') {7 advertise = data8 } else {9 advertise = 'true'10 }11 var reply = {12 "header": {13 "type": "pg"14 },15 "body": {16 "advertise": advertise17 }18 }19 return reply20 })21}However, since the callback is itself a function, this is simply returning reply to where it is called in file.get() rather than returning it back to parseMsg(). I struggled with this issue for quite a while, as I didn't know how to get the data from file.get() to return syncronously.
parseMsg()The solution, it turned out, does not use returns. Going back to parseMsg(), the callback is effectively a function that sends a reply. Therefore, rather than getting each file to return a reply, it is better to pass callback() to each of the type functions. Then, it creates a reply and passes it to the callback, rather than returning it back down to parseMsg()
In this way, the whole process is completely asyncronous as at no point is the program waiting for a function to finish.
The redesigned parseMsg() now catches any errors and appends the received data to a file called error-logs before sending a reply. It also sends ip to pg(), which will be passed to the function when it receives data.
xxxxxxxxxx521function parseMsg(data,ip,callback) {2 // parse incoming messages and replies3 // by calling parse functions4 try {5 var msg = JSON.parse(data)6 if (msg.header.hash === hash.sha256hex(JSON.stringify(msg.body))) {7 if (msg.header.type === 'tx') {8 // transaction9 // callback is used to send the reply10 parse.tx(msg,callback)11 } else if (msg.header.type === 'bk') {12 // block13 parse.bk(msg,callback)14 } else if (msg.header.type === 'hr') {15 // hash request16 parse.hr(msg,callback)17 } else if (msg.header.type === 'cr') {18 // chain request19 parse.cr(msg,callback)20 } else if (msg.header.type === 'pg') {21 // ping22 parse.pg(msg,ip,callback)23 } else if (msg.header.type === 'nr',callback) {24 // node request25 parse.nr(msg,ip,callback)26 } else {27 throw 'type'28 }29 } else {30 throw 'hash'31 }32 } catch(e) {33 // catching any errors and replying with an error message34 console.warn(e)35 var error36 if (e.name === 'SyntaxError') {37 error = 'parse'38 } else {39 error = e40 }41 var reply = {42 "header": {43 "type": "er"44 },45 "body": {46 "error": error47 }48 }49 file.append('error-logs',data)50 callback(reply)51 }52}pg() now looks like this:
xxxxxxxxxx211function pg(msg,ip,callback) {2 // store the connection3 pgreply(msg,ip)4 // send a reply5 file.get('advertise','network-settings',(data) => {6 if (data === 'true' || data === 'false') {7 var advertise = data8 } else {9 var advertise = 'true'10 }11 var reply = {12 "header": {13 "type": "pg"14 },15 "body": {16 "advertise": advertise17 }18 }19 callback(reply)20 })21}I also had to change the server so that it adds the header attributes onto the message, like the time and the size.
xxxxxxxxxx221var server = net.createServer((socket) => {2 var ip = socket.remoteAddress3 socket.setEncoding('utf8')4 // when it receives data, send to parseMsg()5 socket.on('data',(data) => {6 console.log('Received connection from: '+ip)7 console.log('Server received: '+data)8 parseMsg(data,ip,(msg) => {9 if (msg.header.type !== 'tx' && msg.header.type !== 'bk') {10 msg.body['time'] = Date.now()11 msg.header['version'] = version12 msg.header['size'] = Buffer.byteLength(JSON.stringify(msg.body))13 msg.header['hash'] = hash.sha256hex(JSON.stringify(msg.body))14 }15 var reply = JSON.stringify(msg)16 console.info('Sending message to '+ip+': '+reply)17 socket.write(reply)18 file.append('sent',msg.header.hash)19 socket.end()20 })21 })22})tx()Much like pg(), we will be reusing the code to verify transactions, so I split it up into a separate function. This way, it can be used across the program and not just in this context.
transaction()transaction() will take in a message's body, then iterate through the inputs to see if they are valid, using ecdsa.verifyMsg(). The message is the amount plus the recipient's address plus the time. It is a subroutine and does not return anything.
xxxxxxxxxx201function transaction(tx) {2 var from = tx.from3 var len = from.length4 var input5 var concat6 // goes through the transaction inputs7 // and checks that they're all valid8 for (var i; i < len; ++i) {9 input = from[i]10 // this is the "message" for the ecdsa function11 concat = input.amount+tx.to+tx.time12 ecdsa.verifyMsg(concat,input.signature,input.person,(result) => {13 if (result) {14 // it worked15 } else {16 throw 'signature'17 }18 })19 }20}It also needs to check that the wallets aren't overspending. To do this, I used a function that will be created later, blockchain.checkBalance(). We simply pass it the amount and the wallet, and it will return true if the wallet has enough arbitrary units to complete the transaction. However, you could bypass this by using the same wallet twice in the same transaction. Therefore, it throws a parse error if a wallet is repeated by adding the wallets to a list and checking each wallet against the list.
xxxxxxxxxx311function transaction(tx) {2 var from = tx.from3 var len = from.length4 var input5 var concat6 var repeats = []7 // goes through the transaction inputs8 // and checks that they're all valid9 for (var i; i < len; ++i) {10 input = from[i]11 if (repeats.contains(input)) {12 // wallets in a transaction must be unique13 throw 'parse'14 }15 // this is the "message" for the ecdsa function16 concat = input.amount+tx.to+tx.time17 ecdsa.verifyMsg(concat,input.signature,input.person,(result) => {18 if (result) {19 blockchain.checkBalance(input.person,input.amount,(balanceCheck) => {20 if (balanceCheck) {21 repeats.push(input)22 } else {23 throw 'amount'24 }25 })26 } else {27 throw 'signature'28 }29 })30 }31}Now, all tx() need to do is call that subroutine, and then add it to txpool.json, send it on to all the other nodes, and add it to txpool.json, if it's not already there.
xxxxxxxxxx221function tx(msg,callback) {2 var reply = {3 "header": {4 "type": "ok"5 },6 "body": {}7 }8 // verify that it works9 transaction(msg.body)10 // add to txpool11 file.getAll('txpool',(data) => {12 var txpool = JSON.parse(data)13 if (!txpool.includes(msg.body)) {14 file.append('txpool',msg.body,() => {15 // send to contacts16 sendToAll(msg)17 // reply18 callback(reply)19 })20 }21 },'[]')22}bk()As with tx(), I made a separate subroutine to see if a block is valid. This iterates checks the difficulty of a block and then goes through the transactions, calling transaction() for each one. However, I ran into a major problem, in that I could not figure out how to verify the difficulty if it could change. At the moment, the system only lets blocks into blockchain.json if the block is valid. However, with dynamic difficulties, the block could be invalid (by having a difficulty that's too small) but still be in blockchain.json if we don't have it's parent on disk to verify that. This would lead to a lot of issues if the blocks in blockchain.json could not be trusted, and would require an extremely complex restructuring of the whole system, or would require running this subroutine on the whole blockchain every time a block is valid, which would be far too resource-intensive. I could not figure out how to solve this issue, so unfortunatly I had to instead set the difficulty to 6, permanantly. I set the difficulty as a const at the top of the program.
xxxxxxxxxx281function block(body) {2 const difficulty = 63 var txlist = body.transactions4 var len = txlist.length5 var tx6 var blockhash = hash.sha256hex(JSON.stringify(body))7 // verify all the transactions8 var pass = true9 for (var i = 0; i < body.difficulty; i++) {10 if (blockhash.charAt(i) !== 'a') {11 pass = false12 }13 }14 if (body.difficulty === difficulty && pass) {15 for (var i; i < len; ++i) {16 tx = txlist[i]17 try {18 transaction(tx)19 } catch(e) {20 if (e === 'signature' || e === 'amount') {21 throw 'transaction'22 }23 }24 }25 } else {26 throw 'difficulty'27 }28}The bk() function itself is very simple, as it just calls block() then blockchain.addBlock(), a function that will be covered later.
xxxxxxxxxx131function bk(msg,callback) {2 var reply = {3 "header": {4 "type": "ok"5 },6 "body": {}7 }8 block(msg.body)9 // if nothing has been thrown, add to local blockchain10 blockchain.addBlock(msg)11 network.sendToAll(msg)12 callback(reply)13}hr()To construct a bh, we need to use a function that has not yet been covered, blockchain.getTopBlock(). This simply returns the hash of the top block of the blockchain.
xxxxxxxxxx191function hr(msg,callback) {2 file.getAll('blockchain',(data) => {3 if (data === null || data === "{}") {4 throw 'notfound'5 } else {6 blockchain.getTopBlock(JSON.parse(data),(top) => {7 var reply = {8 "header": {9 "type": "bh"10 },11 "body": {12 "hash": top13 }14 }15 callback(reply)16 })17 }18 })19}nr()For a node request, we simply iterate through connections, and add them to an array if "advertise" is "true", the IP is not the same as the node that sent us this message, and if the number of connections is less than the number that was requested. We also need to make sure that if msg.max does not exist, we use Infinity instead since i will always be less than Infinity.
xxxxxxxxxx271function nr(msg,ip,callback) {2 var max = Infinity3 if (msg.hasOwnProperty('max')) {4 var max = msg.max5 }6 var nodes = []7 file.getAll('connections',(data) => {8 if (data === null) {9 throw 'notfound'10 }11 var connections = JSON.parse(data)12 connections.forEach((connection,i) => {13 if (connection.ip !== ip && i < max && connection.advertise === "true") {14 nodes.push(connections)15 }16 })17 var reply = {18 "header": {19 "type": "nd"20 },21 "body": {22 "nodes": nodes23 }24 }25 callback(reply)26 })27}cr()When a client receives a chain request, there are three possibilites:
If it asks for a hash and the client does not have that hash, or if the requested hash is not a part of a complete chain, then it should throw a "notfound" error. If it asks for a specific hash and it has that hash, then the client will call blockchain.getChain() to get the desired chain.
If no hash is requested, then it uses blockchain.mainChain() to get the tallest chain.
xxxxxxxxxx371function cr(msg,callback) {2 if (msg.body.hasOwnProperty('hash')) {3 blockchain.get(msg.body.hash,(block) => {4 if (block === null) {5 throw 'notfound'6 } else {7 blockchain.getChain(msg.body.hash,(chain) => {8 if (chain === null) {9 throw 'notfound'10 } else {11 var reply = {12 "header": {13 "type": "cn"14 },15 "body": {16 "chain": chain17 }18 }19 callback(reply)20 }21 })22 }23 })24 } else {25 blockchain.mainChain((chain) => {26 var reply = {27 "header": {28 "type": "cn"29 },30 "body": {31 "chain": chain32 }33 }34 callback(reply)35 })36 }37}We now need to update the sendMsg() function, so that it automatically adds the time, the version number, the hash and the size to the header and body. It also needs to check that the message has not already been sent by checking the hash against sent.json.
I also had an unexplained bug where it would fail to parse sent.json, so line 6-8 detects and fixes that, as I was not able to find a proper fix.
xxxxxxxxxx521function sendMsg(msg,ip,callback) {2 // for checking that the message hasn't already been sent3 file.getAll('sent',(data) => {4 // for some reason, sent.json sometimes ends with [...]]5 // until I find the source of the bug, this will do6 if (data[data.length-1] === data[data.length-2]) {7 data = data.slice(0,-1)8 }9 var sent = JSON.parse(data)10 if (msg.header.type !== 'bk' && msg.header.type !== 'tx') {11 // don't want to affect the body of a block12 // and the time of the tx is crucial as well13 // as it will throw off the hash14 msg.body['time'] = Date.now()15 }16 msg.header['version'] = version17 msg.header['size'] = Buffer.byteLength(JSON.stringify(msg.body))18 msg.header['hash'] = hash.sha256hex(JSON.stringify(msg.body))1920 // check that the message hasn't already been sent21 if (!sent.includes(msg.header.hash)) {22 var sendMe = JSON.stringify(msg)23 console.info('Sending message to '+ip+': '+sendMe)2425 // actually go send the message26 var client = new net.Socket()27 client.connect(port,ip,() => {28 client.write(sendMe)29 // add the hash to the sent messages file30 file.append('sent',msg.header.hash)31 client.on('data',(data) => {32 console.log('Client received: '+data)33 parseReply(data,ip,() => {34 client.destroy()35 })36 })37 client.on('close',() => {38 })39 client.on('timeout',() => {40 console.warn('Client timed out')41 client.destroy()42 })43 client.on('error',(e) => {44 console.warn('Client timed out')45 client.destroy()46 })47 })48 } else {49 console.log('Message already sent')50 }51 },'[]')52}I also created a function called sendToAll(), which I have used previously. Unsuprisingly, it reads connections.json and sends a message using sendMsg() to each of the nodes.
xxxxxxxxxx121function sendToAll(msg) {2 file.getAll('connections',(data) => {3 // doesn't do anything if there's no connections4 if (data !== null || data === '' || data === '[]') {5 nodes = JSON.parse(data)6 // go through connections and send a message to each7 nodes.forEach((node) => {8 sendMsg(msg,node.ip)9 })10 }11 })12}Now we need to make the equivalent function to parseMsg() for replies, which is parseReply(). It is nearly identical except it doesn't send a reply back, so is simpler. All it does in the case of error is save the message to error-log.json.
xxxxxxxxxx401function parseReply(data,ip,callback=()=>{}) {2 // parse incoming replies3 // by calling parse functions4 try {5 var msg = JSON.parse(data)6 if (msg.header.hash == hash.sha256hex(JSON.stringify(msg.body))) {7 if (msg.header.type === 'cn') {8 // chain9 parse.cn(msg)10 } else if (msg.header.type === 'bh') {11 // top hash12 parse.bh(msg)13 } else if (msg.header.type === 'nd') {14 // nodes15 parse.nd(msg)16 } else if (msg.header.type === 'pg') {17 // ping18 parse.pgreply(msg,ip)19 } else if (msg.header.type === 'ok') {20 // message received ok21 console.info('message recieved ok')22 } else if (msg.header.type === 'er') {23 // error (uh oh)24 console.warn('We recieved an error')25 parse.er(msg)26 } else {27 throw 'type'28 }2930 } else {31 throw 'hash'32 }33 } catch(e) {34 console.warn('Reply error: '+e)35 file.append('error-log',msg)36 } finally {37 // call the callback, if needed38 callback()39 }40}Now we need to make the parsing functions for the replies, in parse.js. I have already covered parse.pgreply() in the previous section.
cn()In theory, we should just be able to iterate through the nodes and add them one by one. However, an oversight in the blockchain.addBlock() function means that it takes in full messages rather than just the body. I worked around this by putting each block into an object as the "body", like so:
xxxxxxxxxx71function cn(msg) {2 for (var key in msg.chain) {3 // an oversight means we need to give it msg.body4 var block = {"body":msg.chain[key]}5 blockchain.addBlock(block)6 }7}nd()When we receive these nodes, we need to make sure that we are not pinging nodes that we are already connected to. This means that we need to get the list of nodes and only send pg messages to those that aren't on both lists.
The first part of this function is simply constructing the pg message.
xxxxxxxxxx351function nd(msg) {2 // some nodes we can connect to3 file.get('advertise','network-settings',(data) => {4 if (data === 'true' || data === 'false') {5 var advertise = data6 } else {7 var advertise = 'true'8 }9 var ping = {10 "header": {11 "type": "pg"12 },13 "body": {14 "advertise": advertise15 }16 }17 file.getAll('connections',(data) => {18 // this must get connection data, as otherwise it wouldn't have received this message19 var connections = JSON.parse(data)20 msg.body.nodes.forEach((node) => {21 var send = true22 // if we are already connected to the node don't send23 connections.forEach((connection) => {24 if (node.ip === connection) {25 send = false26 }27 })28 // otherwise send a ping29 if (send) {30 network.sendMsg(ping,node.ip)31 }32 })33 })34 })35}bh()This is the reply to a hash request, and is the hash of that client's top block. In this function, we need to check if it's in the blockchain, and if not, send out chain requests. To do this, we use !Object.keys(mainchain).includes(msg.body.hash), which gets all the keys from a block and checks to see if msg.body.hash is one of them. If it does, it returns !true, which is false.
xxxxxxxxxx181function bh(msg,callback) {2 file.getAll('blockchain',(data) => {3 var mainchain = JSON.parse(data)4 if (!Object.keys(mainchain).includes(msg.body.hash)) {5 // if the received top hash is not equal to the one on disk6 // and it's not in the blockchain, then send out a chain request7 var chainrequest = {8 "header": {9 "type": "cr"10 },11 "body": {12 "hash": msg.body.hash13 }14 }15 network.sendToAll(chainrequest)16 }17 },'{}')18}ok()I didn't even make an ok() function, I just had it print "Message received ok" into the console back in parseReply().
er()All we need to do here is append the message to error-logs.json.
xxxxxxxxxx31function er(msg) {2 file.append('error-logs',msg)3}Finally, I exported the functions.
xxxxxxxxxx131exports.tx = tx2exports.bk = bk3exports.hr = hr4exports.nr = nr5exports.pg = pg6exports.pgreply = pgreply7exports.nd = nd8exports.bh = bh9exports.cr = cr10exports.er = er11exports.cn = cn12exports.block = block13exports.transaction = transactionWe now need to tackle the blockchain, which is a critical part of the project. It it basically a cool name for a linked list. We need to create the following functions to process it:
To aid with the first function, I decided to structure the blockchain as an object literal rather than an array, with the hash of the block as the key. In this way, we don't need to store the header, and to get a block we simply use blockchain[hash_of_block].
I creates all these functions in a file called blockchain.js. The blockchain itself is stored in blockchain.json, in %APPDATA%. First of all, I created getBlock().
getBlock()This simply uses file.get() to get the block
xxxxxxxxxx51const file = require('./file.js')23function getBlock(hash,callback) {4 file.get(hash,'blockchain',callback)5}Since we don't want to have to trawl through the blockchain to check every input of every transaction, I decided to store just the balances in a new file called balances.json. This file is again a dictionary-style object, with the wallet as the key storing the amount assigned to them. This is much more efficient for getting the amount assigned to a block. We will only need to generate this file when the blockchain changes. To generate it, I created a function called calcBalances()
calcBalances()calcBalances() needs to iterate through all the inputs in each transaction in each block in the blockchain. I decided to use forEach() to do this, as although it is technically slower, it is much clearer to see what is happening. In each input, it sees if balances, the object that stores the balances, contains the wallet referred to in the input, using hasOwnProperty() which returns true if the property has a value assigned to it. If that's case, it deducts the amount defined in the input, and increases the recipient's balance by the same amount.
xxxxxxxxxx301function calcBalances() {2 file.getAll((data) => {3 var chain = JSON.parse(data)4 var balances = {}5 // iterate through the blocks6 for (var key in chain) {7 var block = chain[key]8 transactions = block.transactions9 // iterate through each block to find each transaction10 transactions.forEach((transaction) => {11 // iterate through the inputs12 transaction.from.forEach((from) => {13 // deduct amounts from the inputs14 if (balances.hasOwnProperty(from.wallet)) {15 balances[from.wallet] -= from.amount16 } else {17 balances[from.wallet] = -from.amount18 }19 // add amount to the recipient's balance20 if (balances.hasOwnProperty(transaction.to)) {21 balances[transaction.to] += from.amount22 } else {23 balances[transaction.to] = from.amount24 }25 })26 })27 }28 file.storeAll('balances',balances)29 })30}However, we also need to accound for the mining reward. I set this as a const at the top of the function. Since it is in microau, it is set to 50000000. For each block, we add the mining reward to the miner's wallet.
xxxxxxxxxx371function calcBalances() {2 const miningreward = 500000003 file.getAll((data) => {4 var chain = JSON.parse(data)5 var balances = {}6 // iterate through the blocks7 for (var key in chain) {8 var block = chain[key]9 transactions = block.transactions10 // iterate through each block to find each transaction11 transactions.forEach((transaction) => {12 // iterate through the inputs13 transaction.from.forEach((from) => {14 // deduct amounts from the inputs15 if (balances.hasOwnProperty(from.wallet)) {16 balances[from.wallet] -= from.amount17 } else {18 balances[from.wallet] = -from.amount19 }20 // add amount to the recipient's balance21 if (balances.hasOwnProperty(transaction.to)) {22 balances[transaction.to] += from.amount23 } else {24 balances[transaction.to] = from.amount25 }26 })27 })28 // mining rewards29 if (balances.hasOwnProperty(block.miner)) {30 balances[block.miner] += miningreward31 } else {32 balances[block.miner] = miningreward33 }34 }35 file.storeAll('balances',balances)36 })37}However, we have a problem. In it's current state, it calculates the balance of every block rather than those under the top block, which is incorrect. What we need is a function which gets the top block then returns a subsection of the blockchain containing only blocks under the top block. This function will be called mainChain(), and will be defined later. For now, we will pretend that it exists (as I made these functions at the same time).
xxxxxxxxxx381function calcBalances() {2 const miningreward = 500000003 // mainChain gets the longest chain, as only the blocks under the highest4 // actually count5 mainChain((chain) => {6 var balances = {}7 // iterate through the blocks8 for (var key in chain) {9 var block = chain[key]10 transactions = block.transactions11 // iterate through each block to find each transaction12 transactions.forEach((transaction) => {13 // iterate through the inputs14 transaction.from.forEach((from) => {15 // deduct amounts from the inputs16 if (balances.hasOwnProperty(from.wallet)) {17 balances[from.wallet] -= from.amount18 } else {19 balances[from.wallet] = -from.amount20 }21 // add amount to the recipient's balance22 if (balances.hasOwnProperty(transaction.to)) {23 balances[transaction.to] += from.amount24 } else {25 balances[transaction.to] = from.amount26 }27 })28 })29 // mining rewards30 if (balances.hasOwnProperty(block.miner)) {31 balances[block.miner] += miningreward32 } else {33 balances[block.miner] = miningreward34 }35 }36 file.storeAll('balances',balances)37 })38}getTopBlock()To get the main chain, we need to know what the top block is. Initally, I iterated through them and based it off of height, using time as a tie-break. To make it more flexible, I made it so you have to pass the blockchain to the function to avoid repeating reading the file.
xxxxxxxxxx291function getTopBlock(fullchain,callback) {2 // get the first key in the object3 // doesn't matter if it's best it just needs to be valid4 for (var best in fullchain) {5 // this is the fastest way of getting the first key6 // even if it's kind of messy looking7 // Object.keys(fullchain)[0] puts the whole object into memory8 break9 }10 if (typeof best !== 'undefined') {11 // iterates through the fullchain12 for (var key in fullchain) {13 // larger height the better14 if (fullchain[key].height > fullchain[best].height) {15 best = key16 }17 // otherwise, if they're the same pick the oldest one18 } else if (fullchain[key].height === fullchain[best].height) {19 if (fullchain[key].time < fullchain[best].time) {20 best = key21 }22 }23 }24 }25 } else {26 best = null27 }28 callback(best)29}However, someone could submit a phoney block that has a really high height, without actually being connected to the genesis block through the chain. Whilst it is not the most efficient solution, I decided to only consider a block as the best if I could iterate down to the genesis block, which has parent of '0000000000000000000000000000000000000000000000000000000000000000'. I also made it start with the genesis block.
xxxxxxxxxx571function getTopBlock(fullchain,callback) {2 const genesis = '0000000000000000000000000000000000000000000000000000000000000000'3 // get the origin block4 // as there is nothing under it to be wrong5 for (var best in fullchain) {6 if (fullchain[best].parent === genesis) {7 break8 }9 }10 if (typeof best !== 'undefined' && fullchain[best].parent === genesis) {11 // iterates through the fullchain12 for (var key in fullchain) {13 // larger height the better14 if (fullchain[key].height > fullchain[best].height) {15 var candidate = true16 // iterate down the chain to see if you can reach the bottom17 // if the parent is undefined at any point it is not part of the main chain18 // run out of time for a more efficient method19 var current = key20 var parent21 while (fullchain[current].parent !== genesis) {22 parent = fullchain[current].parent23 if (typeof fullchain[parent] !== 'undefined') {24 current = parent25 } else {26 candiate = false27 }28 }29 if (candidate) {30 best = key31 }32 // otherwise, if they're the same pick the oldest one33 } else if (fullchain[key].height === fullchain[best].height) {34 if (fullchain[key].time < fullchain[best].time) {35 // see other comments36 var candidate = true37 var current = key38 while (fullchain[current].parent !== genesis) {39 parent = fullchain[current].parent40 if (typeof fullchain[parent] !== 'undefined') {41 current = parent42 } else {43 candiate = false44 }45 }46 if (candidate) {47 best = key48 }49 }50 }51 document.getElementById('height').textContent = fullchain[best].height52 }53 } else {54 best = null55 }56 callback(best)57}mainChain()mainChain() pretty much repeats what getTopBlock() does to verify that a block is a part of the chain, except it stores the blocks that it finds, to create a subsection of the chain. Only this part of the chain is valid, which is why it is so important.
xxxxxxxxxx211function mainChain(callback) {2 var mainchain = {}3 file.getAll('blockchain',(data) => {4 if (data === '{}') {5 callback({})6 } else {7 var fullchain = JSON.parse(data)8 getTopBlock(fullchain,(top) => {9 mainchain[top] = fullchain[top]10 var current = top11 var parent12 while (fullchain[current].parent !== '0000000000000000000000000000000000000000000000000000000000000000') {13 parent = fullchain[current].parent14 mainchain[parent] = fullchain[parent]15 current = parent16 }17 callback(mainchain)18 })19 }20 },'{}')21}getChain()getChain() gets a specific part of the chain under a hash passed to it as a parameter, rather than the one given by getTopBlock(). Other than that, it is very similar to mainChain(), other than it has more error handling since it is unknown if the requested chain actually reaches the bottom, unlike in mainChain().
xxxxxxxxxx251function getChain(top,callback) {2 var mainchain = {}3 file.getAll('blockchain',(data) => {4 if (data === '{}') {5 callback(null)6 } else {7 try {8 var fullchain = JSON.parse(data)9 mainchain[top] = fullchain[top]10 var current = top11 var parent12 while (fullchain[current].parent !== '0000000000000000000000000000000000000000000000000000000000000000') {13 parent = fullchain[current].parent14 mainchain[parent] = fullchain[parent]15 current = parent16 }17 } catch(e) {18 console.warn(e)19 mainchain = null20 } finally {21 callback(mainchain)22 }23 }24 },'{}')25}checkBalance()We need a function which checks the balance of a wallet to see if it greater than or equal to some amount. This function is called checkBalance().
xxxxxxxxxx71function checkBalance(key,amount,callback) {2 file.get(key,'balances',(balance) => {3 // returns true if the wallet's balance is4 // less than or equal to the amount requested5 callback(balance >= amount)6 },0)7}addBlock()The addBlock() function is fairly simple, as all it needs to do is check if it's valid, append the block to blockchain.json and then call calcBalances(). However, we also need to remove any transactions from txpool that are in the block. To do this, we iterate through the transactions listed in the block and use splice() and indexOf() to remove the transaction from txpool, if it is there.
We then store txpool.
xxxxxxxxxx201function addBlock(msg) {2 try {3 parse.block(msg.body)4 // if it failed the test, an error will have been thrown5 file.store(hash.sha256hex(JSON.stringify(msg.body)),msg.body,'blockchain')6 console.log('Block added')7 file.getAll('txpool',(data) => {8 var txpool = JSON.parse(data)9 msg.body.transactions.forEach((tx) => {10 // remove pending transactions if they're in the received block11 txpool.splice(txpool.indexOf(tx),1)12 })13 file.storeAll('txpool',txpool)14 calcBalances()15 },'[]')16 } catch(e) {17 console.warn('Block failed:',JSON.stringify(msg))18 console.warn(e)19 }20}parse.block() is used to make sure that it's valid, and will throw an error if it's not - this is why there is a try...catch statment.
Finally, I exported all the functions.
xxxxxxxxxx71exports.get = getBlock2exports.checkBalance = checkBalance3exports.calcBalances = calcBalances4exports.updateBalances = updateBalances5exports.addBlock = addBlock6exports.getTopBlock = getTopBlock7exports.mainChain = mainChainBefore we start creating the pages, I decided to restructure the application slightly. I moved all the Javascript files relating to any of the pages - overview.js, wallets.js etc - to a subfolder in js, pages. I also moved the changePage() function from renderer.js to it's own file, then imported it back into renderer.js. I also modified it to account for the new location of the Javascript files.
xxxxxxxxxx221const remote = require('electron').remote2const fs = require('fs')34function changePage(name) {5 var path = 'pages/' + name + '.html'6 fs.readFile(path,'utf-8',(err, data) => {7 if (err) {8 alert('An error ocurred reading the file: '+name)9 console.warn('An error ocurred reading the file: '+err.message)10 return11 }12 document.getElementById('body').innerHTML = data13 try {14 const pageJS = require('./pages/'+name+'.js')15 pageJS.init()16 } catch(e) {17 console.error(e)18 }19 })20}2122exports.changePage = changePageI imported it back into renderer.js like so:
xxxxxxxxxx11const changePage = require('./js/changepage.js').changePageNow that the Javascript files for pages are a folder deeper than the other files, we have to import these other files like so:
xxxxxxxxxx11const file = require('../file.js')The Wallets page will contain a list of all the wallets that the user has stored. The user can interact with each one. The options they will have will be:
First, we need to store the wallets. Each wallet has four attributes:
The first three are static. However, the money in the wallet is dependent on the blockchain, which we do not want to have to trawl through every time to find the value of each wallet. Therefore, I decided to update the wallets every time calcBalances() is called. It iterates through wallets.json, which is where the wallets will be found, and finds the total amount for each wallet. While doing this, I realised that we could take this opportunity to calculate the balance counter in the corner of the application. For each wallet that is iterated through, the amount calculated for each wallets is added to a counter, and the total in the corner is set to this amount.
Finally, the new wallet's values are stored in wallets.json.
xxxxxxxxxx381function calcBalances() {2 const miningreward = 500000003 // mainChain gets the longest chain, as only the blocks under the highest4 // actually count5 mainChain((chain) => {6 var balances = {}7 // iterate through the blocks8 // removed in this example as nothing has changed9 for (var key in chain) {}10 // calculating the balance in the corner11 file.getAll('wallets',(data) => {12 var wallets = JSON.parse(data)13 var newWallets = []14 var balance = 015 wallets.forEach((wallet) => {16 if (balances.hasOwnProperty(wallet.public)) {17 amount = balances[wallet.public]18 } else {19 amount = 020 }21 // add the au in the wallet to the total balance22 balance += amount23 // and set the balance in the wallet24 newWallets.push({25 "name": wallet.name,26 "public": wallet.public,27 "private": wallet.private,28 "amount": amount29 })30 })31 // change microau to au and set the textcontent of the top left thing32 document.getElementById('current-balance').textContent = balance / 100000033 // save balances34 file.storeAll('wallets',newWallets)35 file.storeAll('balances',balances)36 },'[]')37 })38}Finally, we can start displaying the wallets in wallets.html. The HTML structure of the page is like so:
xxxxxxxxxx91<h1>Transactions</h1>23<h2>Wallets</h2>45<div class="highlight-box">6 <h3>My wallets</h3>7 <button id="create">Create new wallet</button>8 <div class="list" id="wallet-list"></div>9</div>The button with an id of create needs to link to the page where wallets are generated, so we will need to import changePage() into wallets.js. #wallet-list is the html object where we will be adding the wallets. .list is a CSS class like so:
xxxxxxxxxx41.list {2 overflow-y: auto;3 overflow-x: hidden;4}This means that if the contents of #wallet-list is too wide it will simply be hidden, but if it too tall it will create a scroll bar.
Elements within .list will have class .list-item, which has the following properties:
xxxxxxxxxx101.list-item {2 overflow-x: auto;3 width: calc(100% - 12px);4 background-color: white;5 margin: 5px 0;6 padding: 5px;7 border: 1px solid #333;8 border-radius: 5px;9 white-space: nowrap;10}In wallets.js, we first import file and changePage(), and add an event listener to change the page to wallets-create when the button is clicked. This will be the page where we can create a wallet.
xxxxxxxxxx101const file = require('../file.js')2const changePage = require('../changepage').changePage34function init() {5 document.getElementById('create').addEventListener('click',() => {6 changePage('wallets-create')7 })8}910exports.init = initWe now need to populate #wallet-list. This is done by getting the wallets and iterating through them, appending a new div for each wallet, which is done by calling document.createElement() and using appendChild() to place it inside of the #wallet-list element. I also called blockchain.calcBalances() to ensure the wallets are up to date.
xxxxxxxxxx211const file = require('../file.js')2const changePage = require('../changepage').changePage3const blockchain = require('../blockchain.js')45function init() {6 document.getElementById('create').addEventListener('click',() => {7 changePage('wallets-create')8 })9 blockchain.calcBalances()10 file.getAll('wallets',(data) => {11 wallets = JSON.parse(data)12 var walletList = document.getElementById('wallet-list')13 var listItem14 wallets.forEach((wallet) => {15 listItem = document.createElement('div')16 walletList.appendChild(listItem)17 })18 },'[]')19}2021exports.init = initWe now need to add the class .list-item to the child element, and add the data. The first is achieved by calling listItem.classList.add('list-item'). The second part however, using the method above of creating new elements using Javascript and appending them one by one, is cumbersome with the large number of sub-elements we need. Therefore, I decided instead to create a string and use innerHTML, which takes a string instead.
xxxxxxxxxx61wallets.forEach((wallet) => {2 listItem = document.createElement('div')3 listItem.classList.add('list-item')4 listItem.innerHTML = '<p><b>Name:</b> '+wallet.name+'</p><p><b>Public:</b> '+wallet.public+'</p><p><b>Amount:</b> <span class="money">'+wallet.amount/1000000+'</span></p>'5 walletList.appendChild(listItem)6})Notice how wallet.amount is divided by 1000000 - this is because wallet.amount is in au, and we need to convert it to au.
Now, to see if it works, we need to make a way to create wallets.
I created a new page called create-wallets, which was linked to earlier. The HTML for create-wallets is like so:
xxxxxxxxxx111<h1>Transactions</h1>23<h2>Create a Wallet</h2>45<p>Wallet Name:</p>6<input type="text" id="name" placeholder="My Wallet"><br>7<p>Public Key:</p>8<div class="list-item" id="public"></div>9<p>Private Key (DO NOT SHARE!):</p>10<div class="list-item" id="private"></div>11<button id="create">Create Wallet</button>It turns out we can reuse the .list-item class as a kind of highlight box. What wallets-create.js will need to do is populate #public and #private with the appropriate key, then add these and the name to a wallet when #create is clicked.
To do this, we need to use the ecdsa module, as well as file and changePage. This is where we can use ecdsa.createKeys() to create the public and private keys.
xxxxxxxxxx301const ecdsa = require('../ecdsa.js')2const changePage = require('../changepage').changePage3const file = require('../file.js')45function init() {6 ecdsa.createKeys((public, private, err) => {7 if (err) {8 console.error(err)9 changePage('wallets')10 } else {11 document.getElementById('public').innerText = public12 document.getElementById('private').innerText = private13 }14 })15 document.getElementById('create').addEventListener('click',() => {16 var name = document.getElementById('name').value17 console.log('Creating wallet: '+name)18 var data = {}19 data['name'] = name20 data['public'] = document.getElementById('public').textContent21 data['private'] = document.getElementById('private').textContent22 data['amount'] = 023 console.log(JSON.stringify(data))24 file.append('wallets',data,() => {25 changePage('wallets')26 })27 })28}2930exports.init = initWe can now test both wallets and wallets-create. First, I navigated to wallets.

As you can see, there's nothing there. So, I clicked on the "create" button, which takes us to wallets-create.

I entered the name "My Wallet" and pressed "Create", which took me back to the wallets page - except now, it had a wallet called "My Wallet", which means that it worked!

To create transactions, we need to be able to add in multiple input sources. This is much more complex than a single dropdown, as since the input sources will need to be added through Javascript, they can't (easily) have unique ids.
First of all, I created the HTML for the page, in make.html. #error has class .hidden, which gives it property display: none. When we want to display the error message, we remove this class.
Notice how the #inputs div is empty. This is because we will add the dropdowns with in through Javascript.
xxxxxxxxxx181<h1>Transactions</h1>23<h2>Make Transactions</h2>45<div class="hidden" id="error">6 <p><b>Error:</b> missing/incorrect form values</p>7</div>89<form>10 <p>To:</p>11 <input type="text" id="to" placeholder="Address"><br>12 <p>From:</p>13 <div id="inputs">14 </div>15 <button type="button" id="addInput">Add input</button>16 <p>Please note that each wallet can only be used once</p>17 <button type="button" id="send">Send</button>18</form>Now, we need to create make.js. In the init() function, all we do is add event listeners to the buttons. We also import all the necessary functions.
xxxxxxxxxx131const file = require('../file.js')2const parse = require('../parse.js')3const ecdsa = require('../ecdsa.js')4const network = require('../network.js')56function init() {7 var add = document.getElementById('addInput')8 var send = document.getElementById('send')9 add.addEventListener('click',addInput)10 send.addEventListener('click',sendTx)11}1213exports.init = initNow we need to create the function addInput(), which is called when the #addInput button is pressed. It creates a div with the class .input-group. It then adds a dropdown, a line break, and a number input box to that div, and adds a placeholder value to the dropdown. It then appends .input-group to #inputs.
xxxxxxxxxx241function addInput() {2 var inputGroup = document.createElement('div')3 inputGroup.classList.add('input-group')4 // add select5 // <select name="dropdown"></select>6 var select = document.createElement('select')7 select.name = 'dropdown'8 // add placeholder9 select.innerHTML = '<option value="" selected disabled>Choose a wallet</option>'10 // add br11 // <br>12 var br = document.createElement('br')13 // add number input14 // <input name="amount" type="number" placeholder="Amount to send">15 var number = document.createElement('input')16 number.type = 'number'17 number.placeholder = 'Amount to send'18 number.name = 'amount'19 // add them all to the page20 inputGroup.appendChild(select)21 inputGroup.appendChild(br)22 inputGroup.appendChild(number)23 document.getElementById('inputs').appendChild(inputGroup)24}However, we need to put the wallets in the dropdown. For this, I created a new function called populateDropdown(), which opens wallets.json and adds option elements to the dropdown with the wallets' name and balance. It also sets the value of the option to the public key, which is important, as this is what will be returned when we get the value of the dropdown if that option is selected. I also called the function in line 10 of the above snippet.
xxxxxxxxxx131function populateDropdown(select) {2 var option3 // get list of wallets4 file.getAll('wallets',(data) => {5 var wallets = JSON.parse(data)6 wallets.forEach((wallet) => {7 option = document.createElement('option')8 option.value = wallet.public9 option.text = wallet.amount/1000000+"au - "+wallet.name10 select.add(option)11 })12 })13}Finally, I called addInput() in the init() function so that the page starts with an input. You can see that this all worked:

Now, we just need to add the sendTx() function. The reason I had structured the inputs in this way is that I knew that document.getElementsByClassName() gives an array of the elements with that class name. Therefore, we can get all the elements with class name .input-group and iterate through them. We can then use childNodes to get the children of each group, then get their values.
xxxxxxxxxx111function sendTx() {2 var to = document.getElementById('to').value3 var groups = document.getElementsByClassName('input-group')4 groups.forEach((group) => {5 var child = group.childNodes6 var wallet = child[0].value7 var amount = child[1].value8 console.log(wallet)9 console.log(amount)10 })11}However, there are some issues with this - first and foremost, document.getElementsByClassName() does not return an array - it returns a HTMLCollection, which is similar but we can't iterate through it. Luckily, this is easy to solve by turning it into an array using Array.from.
Secondly, there are actually three elements within each group, and therefore child[1] returns the br rather than the input that we want. This is fixed by using child[2] instead.
xxxxxxxxxx161function sendTx() {2 var to = document.getElementById('to').value3 // this isn't an array for some reason4 // we can make it one using Array.from5 // https://stackoverflow.com/a/37941811/54534196 var groups = Array.from(document.getElementsByClassName('input-group'))78 groups.forEach((group) => {9 var child = group.childNodes10 var wallet = child[0].value11 console.log(wallet)12 // 2 because of the br13 var amount = child[2].value14 console.log(amount)15 })16}Now we need to get data from wallets.json in order to sign the inputs. However, since wallets.json is an array, we can't easily get the private key with the public key, so I decided instead to put the wallets in a new format so you can get the private key from the secret key.
xxxxxxxxxx381function sendTx() {2 var to = document.getElementById('to').value3 // this isn't an array for some reason4 // we can make it one using Array.from5 // https://stackoverflow.com/a/37941811/54534196 var groups = Array.from(document.getElementsByClassName('input-group'))7 var message = {8 "header": {9 "type": "tx"10 },11 "body": {12 "to": to,13 "from": []14 }15 }16 file.getAll('wallets',(data) => {17 var time = Date.now()18 message.body['time'] = time19 // converting wallets into a format20 // where you can enter the public key21 // and get the private key22 var convert = {}23 var wallets = JSON.parse(data)24 wallets.forEach((wallet) => {25 public = wallet.public26 private = wallet.private27 convert[public] = private28 })29 groups.forEach((group) => {30 var child = group.childNodes31 var wallet = child[0].value32 console.log(wallet)33 // 2 because of the br34 var amount = child[2].value35 console.log(amount)36 })37 })38}Now we can call convert[public] to get the private key. Next we need to create the signatures etc, which is fairly easy, since we have already created the signMsg() function. However, I put it all in a try-catch statement, and if an error is caught it removes .hidden from #error.
If the message manages to reach the end without errors, it checks the message using parse.transaction, sends the message using sendToAll() and finally appends the message body to both txpool.json and recenttx.json, for mining and for view transaction history, respectively.
xxxxxxxxxx641function sendTx() {2 var to = document.getElementById('to').value3 // this isn't an array for some reason4 // we can make it one using Array.from5 // https://stackoverflow.com/a/37941811/54534196 var groups = Array.from(document.getElementsByClassName('input-group'))7 var message = {8 "header": {9 "type": "tx"10 },11 "body": {12 "to": to,13 "from": []14 }15 }16 file.getAll('wallets',(data) => {17 var time = Date.now()18 message.body['time'] = time19 // converting wallets into a format20 // where you can enter the public key21 // and get the private key22 var convert = {}23 var wallets = JSON.parse(data)24 wallets.forEach((wallet) => {25 public = wallet.public26 private = wallet.private27 convert[public] = private28 })29 try {30 groups.forEach((group) => {31 var child = group.childNodes32 var wallet = child[0].value33 console.log(wallet)34 // 2 because of the br35 var amount = child[2].value36 console.log(amount)37 if (wallet && amount > 0) {38 // convert to microau39 amount *= 100000040 // the message that is signed41 var concat = amount+to+time42 var signature = ecdsa.signMsg(concat,convert[wallet],(signature) => {43 message.body.from.push({44 "wallet": wallet,45 "amount": amount,46 "signature": signature47 })48 })49 } else {50 throw 'no amount entered'51 }52 })53 // if it's invalid, it will throw an error and be caught by the try-catch54 console.log('Transaction: '+JSON.stringify(message))55 parse.transaction(message.body)56 network.sendToAll(message)57 file.append('txpool',message.body)58 file.append('recenttx',message.body)59 } catch(e) {60 document.getElementById('error').classList.remove('hidden')61 console.warn('Tx failed: '+e)62 }63 },'[]')64}This can only really be tested when everything else works, so this will be covered in the full testing phase.
This section is very similar to the wallet viewing page, except it reads recenttx.json instead. The HTML, history.html, looks like this:
xxxxxxxxxx91<h1>Transactions</h1>23<h2>Transaction History</h2>45<div class="highlight-box">6 <h3>Recent Transactions</h3>7 <button id="create">Make transaction</button>8 <div class="list" id="tx-list"></div>9</div>history.js looks like this:
xxxxxxxxxx291const file = require('../file.js')2const changePage = require('../changepage').changePage34function init() {5 document.getElementById('create').addEventListener('click', function () {6 changePage('make')7 })8 file.getAll('recenttx',(data) => {9 transactions = JSON.parse(data)10 var txList = document.getElementById('tx-list')11 var listItem12 if (transactions) {13 transactions.forEach((tx) => {14 var balance = 015 tx.from.forEach((from) => {16 balance += from.amount/100000017 })18 listItem = document.createElement('div')19 listItem.classList.add('list-item')20 // timestamp to date21 var date = new Date(tx.time).toString()22 listItem.innerHTML = '<p><b>Time:</b> '+date+'</p><p><b>To:</b> '+tx.to+'</p><p><b>Amount:</b> <span class="money">'+balance+'</span></p>'23 txList.appendChild(listItem)24 })25 }26 })27}2829exports.init = initSomething of note is the date - since tx.time is a timestamp, we need to turn it into something readable before printing it. For this, we use the Date class. Creating a Date object then using toString() turns it into a human-readable date. Initially, I tried to use toISOString(), which creates this:
xxxxxxxxxx21new Date(Date.now()).toISOString()2"2018-03-14T14:30:01.112Z"However, that is not very readable. I soon discovered that I could use `toString() instead:
xxxxxxxxxx21new Date(Date.now()).toString()2"Wed Mar 14 2018 14:29:49 GMT+0000 (GMT Standard Time)"That is much better, as it is clear what time and date that represents.
Again, since we need to be able to create transactions to see them here, and since we need the blockchain to work in order to do that, we will have to test this at the end.
The blockchain pages are a critical aspect of the project, as it is here where we mine the blockchain.
Mining the blockchain, as mentioned previously, consists of performing hundreds of thousands hash operations to find the one that passes a "difficulty test" - in this case, it passes if the hash begins with a certain number of hashes. Obviously, this is very CPU intensive, and since Node.js is single-threaded this would cripple the performance of the application. This is not desirable, so I looked for alternatives.
The first option I looked at was to see if there was a multithreading module default to Node.js. This lead me to child_process. However, although this appeared to be relevent to my problems, it looked far too complex for this project.
Next, I looked to see if there was anything default to Javascript itself. As it turns out, there is a Webworker API which allows you to run a different JS file independantly from the main program, and also communicate between programs.
An example of a Webworker:
xxxxxxxxxx51var worker = new Worker('worker.js')23worker.onmessage = (msg) => {4 console.log(msg.data)5}However, I immediately ran into a problem when I tried to use Node.js functions in the worker.js file.
xxxxxxxxxx11ReferenceError: require is not definedWebworkers can only use plain Javascript, and don't have access to Node.js modules or features. This is a massive problem, as we need to use cryto module to find the hash of the block at a minimum. I therefore had to carry on looking.
The next place I looked was in npm. Since Worker() was exactly what I needed, I looked for Node.js-compatible alternative.
Luckily enough, I found one. tiny-worker replaces Webworker with the same API but now with access to Node.js functions.
xxxxxxxxxx11>npm install --save tiny-workerThe idea behind the mining page is that we have a pre element acting as a console, and then also have a button to toggle the miner.
xxxxxxxxxx71<h1>Blockchain</h1>23<h2>Mine for Arbitrary Units</h2>45<button id="toggle" style="margin-right:5px">Start</button><button id="clear">Clear</button> <b>Please note:</b> Mining is very CPU intensive67<pre id="console"></pre>I also added a button to clear the console.
The console has the following CSS, to make sure that it is the right size, and has a monospace font.
xxxxxxxxxx131#console {2 box-sizing: border-box;3 width: 100%;4 height: calc(100% - 180px);5 min-height: 300px;6 background-color: #ececec;7 border-radius: 5px;8 border: 1px solid #333;9 padding: 5px;10 overflow-y: auto;11 overflow-x: hidden;12 font-family: 'Courier New', 'Courier', monospace;13}Next I created mine.js in /js/pages.
xxxxxxxxxx271const Worker = require('tiny-worker')2const blockchain = require('../blockchain.js')3const network = require('../network.js')4const file = require('../file.js')56function init() {7 var miner = null8 var button = document.getElementById('toggle')9 var clear = document.getElementById('clear')10 var pre = document.getElementById('console')1112 clear.addEventListener('click',() => {13 pre.innerHTML = ''14 })1516 button.addEventListener('click',() => {17 if (button.textContent == 'Start') {18 pre.innerHTML += 'Start'19 button.textContent = 'Stop'20 } else {21 pre.innerHTML += 'Mining stopped<br>'22 button.textContent = 'Start'23 }24 })25}2627exports.init = initinit() adds event listeners to #toggle and to #clear. #clear simply sets the content of #console to an empty string. For #toggle, I created an if statement that switches the text content of the button between 'Start' and 'Stop'. This way, we can do one thing when the button says "Start" and a different thing when it says "Stop".
The next stage is to create the Worker. If the button is set to "Start", then it checks to see if the miner exists already. If not, it creates a new instance of Worker and sets it to miner. It sets miner's onmessage function to add any received data to #console. If the button is set to "Stop", then it sets miner to null and then adds "Mining stopped" to the #console. After both, it toggles the text content of the button.
xxxxxxxxxx221button.addEventListener('click',() => {2 if (button.textContent == 'Start') {3 if (miner === null) {4 try {5 miner = new Worker('js/mining-script.js')6 miner.onmessage = (msg) => {7 pre.innerHTML += msg.data+'<br>'8 }9 } catch(e) {10 pre.innerHTML = 'Problem starting mining script, sorry :/'11 }12 }13 button.textContent = 'Stop'14 } else {15 if (miner !== null) {16 miner.terminate()17 miner = null18 }19 pre.innerHTML += 'Mining stopped<br>'20 button.textContent = 'Start'21 }22})Next we need to create mining-script.js, in the /js folder. For the time being, I just made it post "Hello World" back to the main program.
xxxxxxxxxx11postMessage('Hello World')Navigating to mine and clicking "Start" gives:

This shows that it works!
We now need to flesh out the mining script. Unfortunately, the following code is very messy, as errors generated in the mining script seemed to disappear or be printed in the command line rather than in the Chromium console, and as such took a great deal of trial and error to get working.
Because of this, I will simply put the final code here rather than go through the process of making it.
As mentioned previously, the difficulty is static, as I could not figure out how to verify it if it could change
xxxxxxxxxx1991const hash = require(__dirname+'/js/hashing.js')2const fs = require('fs')34class Miner {5 constructor(path) {6 const difficulty = 67 this.path = path8 // this is for the printing later9 this.hashes = 010 this.dhash = 011 this.t1 = Date.now()12 this.t2 = Date.now()13 this.tt = Date.now()14 // difficulty is static15 this.block = {16 "header": {17 "type": "bk"18 },19 "body": {20 "difficulty": difficulty21 }22 }2324 var transactions = JSON.parse(fs.readFileSync(this.path+'txpool.json','utf-8'))25 this.block.body['transactions'] = transactions2627 // parent and height28 var top = this.getTopBlock()29 if (top === null) {30 this.block.body['parent'] = '0000000000000000000000000000000000000000000000000000000000000000'31 this.block.body['height'] = 032 } else {33 var blockchain = JSON.parse(fs.readFileSync(this.path+'blockchain.json','utf8'))34 this.block.body['parent'] = top35 this.block.body['height'] = blockchain[top].height+136 }37 // miner38 var wallets = JSON.parse(fs.readFileSync(this.path+'wallets.json','utf-8'))39 var miner = wallets[0].public40 this.block.body['miner'] = miner4142 postMessage('Block formed, mining initiated')43 }4445 mine() {46 // repeatedly hashes with a random nonce47 while (true) {48 this.rand((nonce) => {49 this.block.body['nonce'] = nonce50 // t2 is updated every loop51 this.block.body['time'] = this.t252 this.hashBlock(this.block.body,(hash) => {53 this.hashes++54 this.dhash++55 // checks difficulty56 var pass = true57 for (var i = 0; i < this.block.body.difficulty; i++) {58 if (hash.charAt(i) !== 'a') {59 pass = false60 }61 }62 this.t2 = Date.now()63 // this triggers if the block has passed the difficulty test64 if (pass) {65 postMessage('Hash found! Nonce: '+nonce)66 postMessage(hash)67 postMessage(this.block)68 // get rid of the pending transactions69 fs.writeFileSync(this.path+'txpool.json','[]','utf-8')70 // set the new block things71 this.block.body.transactions = []72 var top = this.getTopBlock()73 this.block.body['parent'] = hash74 this.block.body['height'] += 1 75 } else {76 // printing for the console77 if ((this.t2-this.t1) > 10000) {78 // calculate hashes per second (maybe)79 // *1000 turns it into seconds80 var hs = (this.dhash/(this.t2-this.t1))*100081 this.dhash = 082 this.t1 = Date.now()83 postMessage('Hashing at '+hs.toFixed(3)+' hashes/sec - '+this.hashes+' hashes in '+Math.floor((this.t1-this.tt)/1000)+' seconds')8485 // check to see if the block has updated86 fs.readFile(this.path+'txpool.json','utf-8',(err,content) => {87 if (err) {88 // if the file doesn't exist, set content to []89 if (err.code === 'ENOENT') {90 content = '[]'91 } else {92 postMessage('Error opening file')93 throw err94 }95 }96 var current = JSON.stringify(this.block.body.transactions)97 // change the transactions if they are different98 if (current !== content) {99 var newtx = JSON.parse(content)100 this.block.body['transactions'] = newtx101 postMessage('Transactions updated')102 }103 })104 }105 }106 })107 })108 }109 }110111 rand(callback) {112 callback(Math.floor(10000000000000000*Math.random()))113 }114 115 hashBlock(block,callback) {116 var hashed = hash.sha256hex(JSON.stringify(block))117 callback(hashed)118 }119120 getTopBlock() {121 const genesis = '0000000000000000000000000000000000000000000000000000000000000000'122 try {123 var data = fs.readFileSync(this.path+'blockchain.json','utf8')124 } catch(e) {125 return null126 }127 if (data === '{}' || data === '') {128 return null129 }130 var fullchain = JSON.parse(data)131 // get the origin block132 // as there is nothing under it to be wrong133 for (var best in fullchain) {134 if (fullchain[best].parent === genesis) {135 break136 }137 }138 if (typeof best !== 'undefined' && fullchain[best].parent === genesis) {139 // iterates through the fullchain140 for (var key in fullchain) {141 // larger height the better142 if (fullchain[key].height > fullchain[best].height) {143 var candidate = true144 // iterate down the chain to see if you can reach the bottom145 // if the parent is undefined at any point it is not part of the main chain146 // run out of time for a more efficient method147 var current = key148 var parent149 while (fullchain[current].parent !== genesis) {150 parent = fullchain[current].parent151 if (typeof fullchain[parent] !== 'undefined') {152 current = parent153 } else {154 candiate = false155 }156 }157 if (candidate) {158 best = key159 }160 // otherwise, if they're the same pick the oldest one161 } else if (fullchain[key].height === fullchain[best].height) {162 if (fullchain[key].time < fullchain[best].time) {163 // see other comments164 var candidate = true165 var current = key166 while (fullchain[current].parent !== genesis) {167 parent = fullchain[current].parent168 if (typeof fullchain[parent] !== 'undefined') {169 current = parent170 } else {171 candiate = false172 }173 }174 if (candidate) {175 best = key176 }177 }178 }179 }180 } else {181 best = null182 }183 return best184 }185}186187onmessage = (path) => {188 postMessage('Path recieved')189 try {190 var miner = new Miner(path.data)191 miner.mine()192 } catch(e) {193 postMessage('Error caught')194 if (typeof e !== 'string') {195 e = e.message196 }197 postMessage(e)198 }199}I created a Miner class, which has multiple functions to recreate some of the functions that exist in the main program.
Starting from the top, the first issue I ran into was that this too seemed unable to require modules. As it turns out, I had to put the absolute file path for it to work, which is why I require the hash module like so:
xxxxxxxxxx11const hash = require(__dirname+'/js/hashing.js')__dirname gets the path up to /arbitra-client, and then we concatenate /js/hashing.js to that to get the path to the file we want.
The next issue I ran into was trying to file.js working. I imported it the same way I imported hashing.js, but it kept on throwing an error that was approximately "remote is undefined". This took a great deal of debugging, but I eventually realised that since it was not an Electron renderer process, it did not have access to electron.remote. This was a real issue, as this is required to get the file path for %APPDATA%.
The solution was to get the file path in mine.js, and send it to the mining script using postMessage(). Since network.js also relies on file.js, I realised that in order to send the block it would need to be sent from mine.js. Therefore, in mine.js, I changed the part of the code that initalises the miner to this:
xxxxxxxxxx131miner = new Worker('js/mining-script.js')2miner.onmessage = (msg) => {3 if(typeof msg.data === 'string') {4 pre.innerHTML += 'Hello World'5 } else {6 console.log(JSON.stringify(msg.data))7 blockchain.addBlock(msg.data)8 network.sendToAll(msg.data)9 }10}11// Workers can't get remote so we need to send them the path manually12var path = remote.app.getPath('appData')+'/arbitra-client/'13miner.postMessage(path)In this code, we get the file path that we need, then post a message to the miner. This way, they can get the path without using electron.remote.
When the miner posts a message, it is checked to see if it a string or not. If so, it is printed to #console. If not, it is assumed to be a block and added to the blockchain and sent to all nodes.
First of all, we need to create the constructor of Miner(), which can be seen here. It receives the file path and sets it as a class property. It then creates the block template, and sets all the variables that do not change from block to block. It also sets some properties that are used later to print the hashing rate later on.
An issue I had was similar to the problem I had with the message replies, as the constructor would end with the block still empty. This was because I was misusing callbacks again, and the block would be sent off before the callback had returned. To remedy this issue, instead of using fs.readFile() I used fs.readFileSync(), which as the name suggests returns syncronously. This meant that it had to wait for the data to return, but since this is running on a new thread it does not matter. This is the reason that I had to change getTopBlock() to be syncronous.
xxxxxxxxxx411class Miner {2 constructor(path) {3 const difficulty = 64 this.path = path5 // this is for the printing later6 this.hashes = 07 this.dhash = 08 this.t1 = Date.now()9 this.t2 = Date.now()10 this.tt = Date.now()11 // difficulty is static12 this.block = {13 "header": {14 "type": "bk"15 },16 "body": {17 "difficulty": difficulty18 }19 }2021 var transactions = JSON.parse(fs.readFileSync(this.path+'txpool.json','utf-8'))22 this.block.body['transactions'] = transactions2324 // parent and height25 var top = this.getTopBlock()26 if (top === null) {27 this.block.body['parent'] = '0000000000000000000000000000000000000000000000000000000000000000'28 this.block.body['height'] = 029 } else {30 var blockchain = JSON.parse(fs.readFileSync(this.path+'blockchain.json','utf8'))31 this.block.body['parent'] = top32 this.block.body['height'] = blockchain[top].height+133 }34 // miner35 var wallets = JSON.parse(fs.readFileSync(this.path+'wallets.json','utf-8'))36 var miner = wallets[0].public37 this.block.body['miner'] = miner3839 postMessage('Block formed, mining initiated')40 }41}These following functions are a part of the Miner class.
To generate a random number for the nonce, I decided to put a wrapper around Math.random() so that it generates integers, by multiplying it by a large number. I decided to use Math.random() rather than the cryptographically secure alternative because being fast is more important than being completely unpredictable, as it just needs to be different from other people's guesses.
xxxxxxxxxx31rand(callback) {2 callback(Math.floor(10000000000000000*Math.random()))3}Although mostly unneccessary, I wrapped hash.sha256hex() so that I could feed it objects and it would stringify() it.
xxxxxxxxxx41hashBlock(block,callback) {2 var hashed = hash.sha256hex(JSON.stringify(block))3 callback(hashed)4}Since we can't use blockchain.getTopBlock() as it uses file.js, I had to repeat it in the Miner class. I also changed it so that it was syncronous, as explained earlier.
xxxxxxxxxx651getTopBlock() {2 const genesis = '0000000000000000000000000000000000000000000000000000000000000000'3 try {4 var data = fs.readFileSync(this.path+'blockchain.json','utf8')5 } catch(e) {6 return null7 }8 if (data === '{}' || data === '') {9 return null10 }11 var fullchain = JSON.parse(data)12 // get the origin block13 // as there is nothing under it to be wrong14 for (var best in fullchain) {15 if (fullchain[best].parent === genesis) {16 break17 }18 }19 if (typeof best !== 'undefined' && fullchain[best].parent === genesis) {20 // iterates through the fullchain21 for (var key in fullchain) {22 // larger height the better23 if (fullchain[key].height > fullchain[best].height) {24 var candidate = true25 // iterate down the chain to see if you can reach the bottom26 // if the parent is undefined at any point it is not part of the main chain27 // run out of time for a more efficient method28 var current = key29 var parent30 while (fullchain[current].parent !== genesis) {31 parent = fullchain[current].parent32 if (typeof fullchain[parent] !== 'undefined') {33 current = parent34 } else {35 candiate = false36 }37 }38 if (candidate) {39 best = key40 }41 // otherwise, if they're the same pick the oldest one42 } else if (fullchain[key].height === fullchain[best].height) {43 if (fullchain[key].time < fullchain[best].time) {44 // see other comments45 var candidate = true46 var current = key47 while (fullchain[current].parent !== genesis) {48 parent = fullchain[current].parent49 if (typeof fullchain[parent] !== 'undefined') {50 current = parent51 } else {52 candiate = false53 }54 }55 if (candidate) {56 best = key57 }58 }59 }60 }61 } else {62 best = null63 }64 return best65}Finally, the most important function mine(). When this is called, it will run indefinitely, hashing the block forever.
It first calls our rand() function to get the nonce, and adds the nonce to the block, as well as the current time. It then checks the difficulty by hashing the block and iterating through the hash. If it begins with as many as as is stated in the block, then it passes. If so, it sends a message saying that the hash has been found, along with the nonce and the hash. It then sends the block, which will be sent on from mine.js. It then clears txpool.json, as those messages have now been sent, and increases the height and changes the parent to the hash that was just found.
xxxxxxxxxx351mine() {2 // repeatedly hashes with a random nonce3 while (true) {4 this.rand((nonce) => {5 this.block.body['nonce'] = nonce6 // t2 is updated every loop7 this.block.body['time'] = this.t28 this.hashBlock(this.block.body,(hash) => {9 this.hashes++10 this.dhash++11 // checks difficulty12 var pass = true13 for (var i = 0; i < this.block.body.difficulty; i++) {14 if (hash.charAt(i) !== 'a') {15 pass = false16 }17 }18 this.t2 = Date.now()19 // this triggers if the block has passed the difficulty test20 if (pass) {21 postMessage('Hash found! Nonce: '+nonce)22 postMessage(hash)23 postMessage(this.block)24 // get rid of the pending transactions25 fs.writeFileSync(this.path+'txpool.json','[]','utf-8')26 // set the new block things27 this.block.body.transactions = []28 var top = this.getTopBlock()29 this.block.body['parent'] = hash30 this.block.body['height'] += 1 31 }32 })33 })34 }35}However, I also wanted it to print to the console occasionally to show that it was doing something. After much trial and error, I found the best way to do this was to use this.t1 and this.t2. t2 approximately the current time, and it is updated after every hash. t1 is set in the constructor. Therefore, finding this.t2-this.t1 will give time time in milliseconds since it started mining. I used this trigger code to run every 10 seconds - if this.t2-this.t1 is larger than 10000 milliseconds, then it resets t1 to the current time, and prints the hashing rate as well as the total number of hashes to the #console.
xxxxxxxxxx161if (pass) {2 postMessage('Hash found! Nonce: '+nonce)3 postMessage(hash)4 postMessage(this.block)5 // etc6} else {7 // printing for the console8 if ((this.t2-this.t1) > 10000) {9 // calculate hashes per second (maybe)10 // *1000 turns it into seconds11 var hs = (this.dhash/(this.t2-this.t1))*100012 this.dhash = 013 this.t1 = Date.now()14 postMessage('Hashing at '+hs.toFixed(3)+' hashes/sec - '+this.hashes+' hashes in '+Math.floor((this.t1-this.tt)/1000)+' seconds')15 }16}To find how fast hashes are being generated, we use this.dhash, which is the number of hashes that have happened since the last interval. Dividing that by this.t2-this.t1 then multiplying by 1000 gives the number of hashes per second. I cropped that to 3 decimal places by using toFixed(3). I found the total time since it started hashing using tt, which is set in the constructor and not changed. t1-tt gives the time in milliseconds since the constructor was called, which I then converted to seconds and rounded.
This is also a good opportunity to see if any more transactions have been added to txpool.json. We read that file, check to see if it's different, and if it is then we change the transactions in the block. The reason I decided to only do this every ten seconds is that doing that for every hash would slow the whole thing down with the reading operations, and would wear out the hard drive.
xxxxxxxxxx191// check to see if the block has updated2fs.readFile(this.path+'txpool.json','utf-8',(err,content) => {3 if (err) {4 // if the file doesn't exist, set content to []5 if (err.code === 'ENOENT') {6 content = '[]'7 } else {8 postMessage('Error opening file')9 throw err10 }11 }12 var current = JSON.stringify(this.block.body.transactions)13 // change the transactions if they are different14 if (current !== content) {15 var newtx = JSON.parse(content)16 this.block.body['transactions'] = newtx17 postMessage('Transactions updated')18 }19})I tested this by navigating to the mine page and clicking "Start". After 30 seconds:

After 420 seconds, it still had not found a block. This either means that the difficulty test doesn't work, or the difficulty is too high.

I will investigate this later on. However, this does show that the printing function works.
Viewing the blockchain is very similar to the history page and wallets. The HTML, view.html, looks like this:
xxxxxxxxxx91<h1>Blockchain</h1>23<h2>View Blockchain</h2>45<div class="highlight-box">6 <h3>Blockchain</h3>7 <button id="mine-button">Mine</button>8 <div class="list" id="bk-list"></div>9</div>view.js looks like this:
xxxxxxxxxx281const file = require('../file.js')2const blockchain = require('../blockchain.js')3const changePage = require('../changepage').changePage45function init() {6 document.getElementById('mine-button').addEventListener('click',() => {7 changePage('mine')8 })9 file.getAll('blockchain',(data) => {10 var chain = JSON.parse(data)11 var list = document.getElementById('bk-list')12 var listItem13 var block14 for (var hash in chain) {15 block = chain[hash]16 listItem = document.createElement('div')17 listItem.classList.add('list-item')18 // timestamp to date19 var date = new Date(block.time).toString()20 // pretty printing json21 var txs = JSON.stringify(block.transactions,null,4)22 listItem.innerHTML = '<p><b>Time:</b> '+date+'</p><p><b>Hash:</b> '+hash+'</p><p><b>Parent:</b> '+block.parent+'</p><p><b>Miner:</b> '+block.miner+'</p><p><b>Height:</b> '+block.height+'</p><p><b>Transactions:</b></p><pre>'+txs+'</pre>'23 list.appendChild(listItem)24 }25 })26}2728exports.init = initThe only notable differences is that since blockchain.json is not an array, I used a for...in loop which gets the keys of the object.
I also printed the transactions as raw JSON in a pre element. When stringify()ing the JSON data, I gave it the extra parameters of null and 4 which should indent it with 4 spaces.
This needs the blockchain to work, so I will cover it in the Testing phase.
The things I wanted to be able to do from network settings are:
advertise in the ping messagesThe first feature was already made for testing.js, so we can just copy that over. The next three change a file when a button is pressed.
As such, network-settings.html looks like this:
xxxxxxxxxx301<h1>Settings</h1>23<h2>Network Settings</h2>45<h3>Add node</h3>6<p>Enter an IP address, and it will attempt to connect.</p>7<input type="text" id="sendto"/>8<button id="send">Send ping</button>9<p id="pg-save" class="hidden">Ping sent</p>1011<h3>Target connections</h3>12<p>The target number of connections that the client will try to get. Current: <span id="curr"></span></p>13<input type="number" id="target"/>14<button id="target-save">Save</button>15<p id="min-save" class="hidden">Option saved</p>1617<h3>Advertise</h3>18<p>Sometimes, nodes will ask others to send a list of clients that they are in contact with. Your IP will only be shared if this setting is turned on.</p>19<select id="advertise">20 <option value="true">On</option>21 <option value="false">Off</option>22</select>23<button id="save">Save</button>24<p id="ad-save" class="hidden">Option saved</p>252627<h3>Refresh connections</h3>28<p>Refresh this client's connections to other nodes.</p>29<button id="refresh">Refresh</button>30<p id="re-save" class="hidden">Connections refreshed</p>I added messages confirming that the action has been completed, but are hidden by default. When the associated action is completed, .hidden will be removed confirming it did something.
Next, I created network-settings.js. The outer section looks like this:
xxxxxxxxxx111const network = require('../network.js')2const file = require('../file.js')34function init() {5 // setting the current target connections6 file.get('target-connections','network-settings',(target) => {7 document.getElementById('curr').textContent = target8 })9}1011exports.init = initThis sets the span #curr with the current connection target that we have on file.
To ping a node, all we need to do is get advertise from network-settings.json, use that to form a ping message, then send that to whatever is in the text box using network.sendMsg().
When that is done, it removes .hidden from #pg-save,
xxxxxxxxxx151// ping an IP2document.getElementById('send').addEventListener('click',() => {3 file.get('advertise','network-settings',(data) => {4 var msg = {5 "header": {6 "type": "pg"7 },8 "body": {9 "advertise": data10 }11 }12 network.sendMsg(msg,document.getElementById('sendto').value)13 document.getElementById('pg-save').classList -= 'hidden'14 })15})"Target connections" is a number that indicates how many connections the network should try to attain. If the number of connections is less that this, it will send out node requests until it reaches that number. By default this is 5.
This code simply uses file.store() to store the number in #target to network-settings.json.
xxxxxxxxxx81// saving the "target number of connections"2document.getElementById('target-save').addEventListener('click',() => {3 var min = document.getElementById('target').value4 file.store('target-connections',min,'network-settings',() => {5 document.getElementById('curr').textContent = min6 document.getElementById('min-save').classList -= 'hidden'7 })8})advertise is the variable used when creating error messages. All we need to do is get a value from the dropdown and store it in network-settings.json.
xxxxxxxxxx71// saving the advertise toggle2document.getElementById('save').addEventListener('click',() => {3 var options = document.getElementById('advertise')4 file.store('advertise',options.value,'network-settings',() => {5 document.getElementById('ad-save').classList -= 'hidden'6 })7})This simply wipes connections.json and calls the connect() function. It also sets #connections, the counter in the corner, to 0.
xxxxxxxxxx71// refreshing the cache2document.getElementById('refresh').addEventListener('click',() => {3 file.storeAll('connections','[]')4 document.getElementById('connections').textContent = 05 network.connect()6 document.getElementById('re-save').classList -= 'hidden'7})Application settings is even simpler, as all I can think to put in it is:
%APPDATA%wallets.json to somewhere elseapp-settings.html looks like this:
xxxxxxxxxx141<h1>Settings</h1>23<h2>App Settings</h2>45<p>Application Version: <span id="version"></span></p>67<h3>Save wallets</h3>8<p>Save wallets.json to somewhere in your computer.</p>9<button id="save">Save</button>1011<h3>Clear cached file</h3>12<p>Warning - this will delete any pending transactions, and you will have to redownload the blockchain. It will not delete your wallets.</p>13<button id="clear">Clear cache</button>14<p id="ca-save" class="hidden">Cache cleared</p>The outer Javascript looks like this:
xxxxxxxxxx111const file = require('../file.js')2const version = require('../../package.json').version3const fs = require('fs')4const network = require('../network.js')5const dialog = require('electron').remote.dialog67function init() {8 // stuff goes here9}1011exports.init = initThere are some extra inports in this file. We import both fs and file.js, because file.js can only store data in %APPDATA%. dialog is a part of Electron, and as such we need to access it through electron.remote.
I retrieved the version number by using require() on package.json, which is where that is stored. Getting the property version from that gives us the application version directly. We then set the version number using:
xxxxxxxxxx11document.getElementById('version').textContent = versionNext we need to save wallets.json. When the button is pressed, it uses file.getAll() to get wallets.json. It then opens a save dialog using dialog.showSaveDialog(). The "filters" are used for the extension dropdown thing in Windows. In the callback we get the file path, and use fs.writeFile() to save wallets.json to that file.
xxxxxxxxxx151document.getElementById('save').addEventListener('click',() => {2 file.getAll('wallets',(data) => {3 dialog.showSaveDialog({4 filters: [5 // set default extensions6 {name:'JSON',extensions:['json']},7 {name:'All files',extensions:['*']}8 ]9 },(file) => {10 fs.writeFile(file,data,(err) => {11 if (err) throw err12 })13 })14 })15})This solution used http://mylifeforthecode.com/getting-started-with-standard-dialogs-in-electron/
Next is the "clear cache" button. This sets all the files to their empty values, which is self-explanatory. It then sets #connections to 0 and calls network.connect().
xxxxxxxxxx151document.getElementById('clear').addEventListener('click',() => {2 file.storeAll('blockchain',{})3 file.storeAll('balances',{})4 file.storeAll('connections',[])5 file.storeAll('network-settings',{"advertise":"true","target-connections":5})6 file.storeAll('recent-connections',[])7 file.storeAll('txpool',[])8 file.storeAll('recenttx',[])9 file.storeAll('sent',[])10 file.storeAll('error-log',[])11 document.getElementById('ca-save').classList.remove('hidden')12 document.getElementById('connections').textContent = 013 console.warn('All files wiped')14 network.connect(false)15})Notice that network-settings.json has it's default values set.
These are minor improvements that were not major enough to warrent their own section.
I moved the CSS to a new folder called /static, as well as the icons. I also downloaded the font-awesome file to this folder. I then changed index.html to this:
xxxxxxxxxx71<head>2 <meta charset="utf-8">3 <title>Arbitra Client</title>4 <link rel="stylesheet" href="static/style.css"/>5 <script defer src="renderer.js" type="text/javascript"></script>6 <script defer src="static/fontawesome.min.js"></script>7</head>I also discovered that the defer tag exists, which only loads the Javascript file when the rest of the page is finished, so I moved renderer.js up to the header.
In the original concept, I had included several counters beneath the balance total. I had removed all of these except #connections as that was the only one that worked. However, I liked having multiple counters so I decided to add one back in. Since it is easy to get the height of the top block, I decided to display the length of the blockchain.
I changed index.html to this:
xxxxxxxxxx41<ul>2 <li><i class="fa fa-fw fa-rss" aria-hidden="true"></i> <span id="connections">0</span> connections</li>3 <li><i class="fa fa-fw fa-chain" aria-hidden="true"></i> <span id="height">0</span> blocks in blockchain</li>4</ul>.fa-chain is a chain icon. I also added .fa-fw to both icons so that they are a fixed width.
This looks like this:

Then, at the end of blockchain.getTopBlock(), I updated the counter
xxxxxxxxxx131 2 if (candidate) {3 best = key4 }5 }6 }7 document.getElementById('height').textContent = fullchain[best].height8 }9 } else {10 best = null11 }12 callback(best)13}I also set it to 0 in the "clear cache" function in app-settings.
I wanted for the application to have an icon, as otherwise the Electron logo is used. I made this in paint in about 30 seconds:

I stored this in /static.
In main.js, I set the icon.
xxxxxxxxxx61win = new BrowserWindow({2 width: 1280,3 height: 720,4 frame: false,5 icon: 'static/au-icon.png'6})Something that had irritated me through development is that the application displays the HTML before the Javascript is fully loaded, so there is a time when the buttons are unresponsive but there is no indication that that is the case.
However, since #body is blank until the Javascript loads, after which it is replaced by the associated page, I realised that I could set #body to display the icon, and it would disappear when the Javascript loads. This visually indicates that the application is loaded.
I therefore added the icon to index.html
xxxxxxxxxx31<div id="body">2 <img src="static/au-icon.png" alt="splash image">3</div>I added the following CSS to centralise it:
xxxxxxxxxx41#body > img {2 margin-top: calc(50vh - 100px);3 margin-left: calc(50% - 50px);4}I then started the application to test it.

It worked, and was replaced by the overview page after a couple of seconds.
Since overview.js doesn't do anything, and since it is loaded every time the application starts, I realised it would be a good place to make sure that all the JSON files exist. It is pretty much the same as the function in app-settings.js, but only sets a file to their empty state if they don't exist.
xxxxxxxxxx471const file = require('../file')2const blockchain = require('../blockchain.js')3function init() {4 // since it runs when you start the program5 // might as well check all the files exist6 file.getAll('txpool',(data) => {7 if (data === null || data === '') {8 file.storeAll('txpool',[])9 }10 })11 file.getAll('recenttx',(data) => {12 if (data === null || data === '') {13 file.storeAll('recenttx',[])14 }15 })16 file.getAll('network-settings',(data) => {17 if (data === null || data === '') {18 var defaults = {19 "advertise": "true",20 "target-connections": 521 }22 file.storeAll('network-settings',defaults)23 }24 })25 file.getAll('blockchain',(data) => {26 if (data === null || data === '' || data === '[]') {27 file.storeAll('blockchain',{})28 }29 })30 file.getAll('balances',(data) => {31 if (data === null || data === '' || data === '[]') {32 file.storeAll('blockchain',{})33 }34 })35 file.getAll('connections',(data) => {36 if (data === null || data === '') {37 file.storeAll('connections',[])38 }39 })40 file.getAll('recent-connections',(data) => {41 if (data === null || data === '') {42 file.storeAll('recent-connections',[])43 }44 })45}4647exports.init = initI also realised that if a person tries to mine the blockchain without a wallet, it would break. Therefore, if wallets.json is empty, I generated a new wallet called "My Wallet". This way, everyone starts with a wallet.
xxxxxxxxxx181file.getAll('wallets',(data) => {2 if (data === null || data === '' || data === '[]') {3 ecdsa.createKeys((public, private, err) => {4 if(err) {5 console.error(err)6 alert(err)7 } else {8 var wallet = {9 "name": "My Wallet",10 "public": public,11 "private": private,12 "amount": 013 }14 file.storeAll('wallets',[wallet])15 }16 })17 }18})I also wanted the HTML on the page to be a helpful introduction on what is possible with Arbitra. Therefore, I changed overview.html to this:
xxxxxxxxxx161<h1>Arbitra</h1>23<p>Welcome to Arbitra!</p>45<br>67<h3>You can:</h3>89<p><i class="fa fa-fw fa-envelope" aria-hidden="true"></i> Send a transaction</p>10<p><i class="fa fa-fw fa-chain" aria-hidden="true"></i> Mine the blockchain for Arbitrary Units</p>11<p><i class="fa fa-fw fa-eye" aria-hidden="true"></i> View past transactions and the blockchain</p>12<p><i class="fa fa-fw fa-rss" aria-hidden="true"></i> Connect to other nodes on the network</p>1314<br>1516<p>Got any questions/feedback? Email me at <a href="mailto:hello@samuelnewman.uk">hello@samuelnewman.uk</a></p>Which looks like this:

To save me running .\node_modules\.bin\electron . every time, I added a script to package.json:
xxxxxxxxxx191{2 "name": "arbitra-client",3 "version": "0.2.0",4 "main": "main.js",5 "license": "MIT",6 "repository": {7 "type": "git",8 "url": "git://github.com/Mozzius/arbitra.git"9 },10 "dependencies": {11 "big-integer": "^1.6.26",12 "electron": "^1.8.3",13 "ip": "^1.1.5",14 "tiny-worker": "^2.1.2"15 },16 "scripts": {17 "start": ".\\node_modules\\.bin\\electron ."18 }19}This means that I can use npm start in the console to start the application.
In this section, I will attempt to verify that the application and the network work to the standard of the inital objectives.
For some of the tests, I installed Arbitra on a friend's PC, which was running 24/7 anyway for their project. Luckily for me, they had a static IP, so I used that for the backup server.
arbitra-client folder in %APPDATA%\Roaming\arbitra-clientnpm startoverview page is automatically openedSuccess


wallets pagewallets-create page opensSuccess


network-settings pagepg message is sent to the correct IP on port 2018sent.jsonpg messageconnections.jsonSuccess


connections.json:
xxxxxxxxxx11[{"ip":"5.81.186.90","advertise":"true"}]connections.jsonSuccess

app-settings and click "Clear cache"Success

mine pageok messageSuccess with minor visual bug

I missed the first block it mined, so here is the second block



Since the height of a block is zero-indexed, the block length counter in the top left has an off by one error.
In blockchain.js, change:
xxxxxxxxxx11document.getElementById('height').textContent = fullchain[best].heightto:
xxxxxxxxxx11document.getElementById('height').textContent = fullchain[best].height + 1make pagehistory pageSuccess



txpool.json:
xxxxxxxxxx11[{"to":"bafad16bb7479e2827859c489a38c0bedeef96ce8a1aec201901394d16d1783b-bbddd2a5ef17608dfed16b2d351398ee3d208e215129dfa02b777ee2c801dcc0","from":[{"wallet":"ad003b2393f396d69540886ebf5ab888f0c89e64cbb8415b5ad6ac1a10f890f77c9ec603e255437e6daffe3ed0c67c41f9798778eec952e5214acaa4a6762a16","amount":25000000,"signature":"6bfe755218cd424bfe452e55ccb347604cf0e5c92b238a3f57b1b65e7b3211c51dfa483fcf64e236253256283000b79f582ba151a02a2281acb0af953ca1f5c"}],"time":1521399948482}]mine pageSuccess


app-settings pagewallets.json is saved to the place selectedSuccess

C:\Users\Mozzi\Documents\wallets.json:
xxxxxxxxxx11[{"name":"My Wallet","public":"71870e4352b2d266cbac8ffa88cc92c1b9c93b9c532ad972fa066404bb080010-beae4c7d8995824d2d600d09b4e4784a6a013973202154e28b134ad2c3efc937","private":"732e33449bf91ae9dcf4d7eef791301280bd64935b4cee6854d0d582db7bcd69","amount":100},{"name":"Test Wallet","public":"6bcfc9193ac2b8db237583afa9bb8a347f28ea1af6fb78856f077d3569a26e074fb62fbf8e7f8ad4842ef39da8490953b87b1492c4c219cdfe83a2743dbdc01e","private":"aee7882724828734665fd7810b4caaedbfbaf2e53c713bd919f98dd18657caf4","amount":0}]make pageSuccess


| Objective | Met? | Comment |
|---|---|---|
| The user should be able to construct and send a valid transaction. | Yes | This does work, provided the wallet has the funds required. |
| The program should be able to automatically parse, validate, and deal with messages. | Yes | The parseMsg() calls functions with deal with all of the message types. |
| The user should be able to mine the blockchain. | Yes | The user can mine the blockchain using the mine page. |
| Users should be rewarded for mining the blockchain. | Yes | Users are rewarded 50au per block. |
| All transactions should be secured through the Elliptic Curve Digital Signature Algorithm. | Yes | An ECDSA system was implemented and used. |
| The user should be able to see sent transactions, their wallets, and the blockchain. | Yes | Users can view sent transactions, their wallets, and the blockchain through the corresponding pages. |
| The user should be able to change basic settings. | Yes | There is both an app-settings and network-settings page with multiple options. |
| The program should connect to other clients automatically, and default to a IP that is running the program. | Yes | Ping messages are automatically sent, with a friend's computer running as a backup node. |
| The program should be able to detect and reject invalid messages. | Mostly | It detects obviously incorrect messages, but the system lacks sufficient depth and can easily be tricked. |
| The blockchain should function as described by the previous section. | Mostly | The blockchain is a blockchain, but the difficulty is static. |
| The user should be able to interact with the program through an easy-to-use UI. | Yes | The UI is implemented using Electron and is easy-to-use. |
| The user should be able to save their wallets. | Yes | Users can save wallets through the app-settings page. |
Whilst as a project, Arbitra has been very successful, I do not feel that I have made a cryptocurrency worth using. Even though it fulfilled all of the project goals in some capacity, the system is far too unstable. The point of a cryptocurrency is that it is built on trusting the mathematics and the protocols of which it consists, and whilst the maths may hold up (hopefully) the protocols are buggy and incomplete. My biggest issue was in the networking which I thoroughly underestimated the difficulty of, which meant that it took focus away from other parts of the app, most notable the lack of a dynamic difficulty.
If I were to start over, my biggest focus would be on ensuring that the protocol was fully planned out and tested. There is a surprising lack of information about running peer-to-peer networks using Node.js, and so a lot of the protocol was guesswork based on preconceptions about how things worked. I would also ensure that the mining system was a bit more planned out, as the unsolved bug in the testing phase proved. I would, however, have used the same approach of using Node.js and Electron, as they were powerful and well-suited for the task, for the most part.
I also am disappointed in the lack of focus on functions that deal with the blockchain. Whilst they work, they are very inefficient and I had to brute-force many tasks due to time constraints.
I am very happy with the UI (although it is irritating I couldn't find icons that fit with the Windows aesthetic), and I am very pleased with how the page system worked out. The code is modular and flexible, and I am happy that I did not need any libraries to deal with the UI except Electron. In fact, a personal goal was not to use modules outside of the standard library where possible, and I managed to end up with only four dependencies, including Electron. Whilst this was probably not the best idea if I was trying to make an actual cryptocurrency, it made me deal with a lot of things on a very low level, especially the cryptography, and ultimately made the project more interesting.
If I had more time, I would:
.exeI had attempted to build the project, but however it was far more complex that anticipated and I had to settle with the npm start script due to time constraints.
Stuart Evans, a fellow student, had this feedback:
The cryptocurrency Arbitra is currently in a robust state, but however as there is no market for it, the currency has no value. As with any cryptocurrency, it requires people to use it in order for it to work. However, it has potential and is a solid starting point from which a cryptocurrency could develop.
The UI is pretty and very usable, however it often lacks feedback when performing tasks.
I think this is reasonable criticism. Getting people to use the cryptocurrency is hard given it's rough state at the moment, however this could be solved with a more interactive user interface (for example, showing what state a transaction is in, progress bars et cetera). This would have to be considered if more time was available.
Here is all of the code in the project, organised by file structure. I have excluded /node_modules, README.md, the licence, and git-related files.
package.json:
xxxxxxxxxx191{2 "name": "arbitra-client",3 "version": "0.2.0",4 "main": "main.js",5 "license": "MIT",6 "repository": {7 "type": "git",8 "url": "git://github.com/Mozzius/arbitra.git"9 },10 "dependencies": {11 "big-integer": "^1.6.26",12 "electron": "^1.8.3",13 "ip": "^1.1.5",14 "tiny-worker": "^2.1.2"15 },16 "scripts": {17 "start": ".\\node_modules\\.bin\\electron ."18 }19}main.js:
x
1const { app, BrowserWindow, globalShortcut } = require('electron')2const file = require('./js/file.js')3const path = require('path')4const url = require('url')56// Keep a global reference of the window object, if you don't, the window will7// be closed automatically when the JavaScript object is garbage collected.8let win910function createWindow() {11 // Create the browser window.12 win = new BrowserWindow({13 width: 1280,14 height: 720,15 frame: false,16 icon: 'static/au-icon.png'17 })1819 // and load the index.html of the app.20 win.loadURL(url.format({21 pathname: path.join(__dirname,'index.html'),22 protocol: 'file:',23 slashes: true24 }))2526 // Open the DevTools when Alt is pressed27 globalShortcut.register('Alt+X',() => {28 win.webContents.openDevTools()29 })3031 // Emitted when the window is closed.32 win.on('closed',() => {33 // Dereference the window object, usually you would store windows34 // in an array if your app supports multi windows, this is the time35 // when you should delete the corresponding element.36 win = null37 })38}3940// This method will be called when Electron has finished41// initialization and is ready to create browser windows.42// Some APIs can only be used after this event occurs.43app.on('ready',createWindow)4445// Quit when all windows are closed.46app.on('window-all-closed',() => {47 // It quits when all windows are closed, regardless of platform.48 app.quit()49})5051app.on('activate',() => {52 // On macOS it's common to re-create a window in the app when the53 // dock icon is clicked and there are no other windows open.54 if (win === null) {55 createWindow()56 }57})renderer.js:
xxxxxxxxxx681const remote = require('electron').remote2const changePage = require('./js/changepage.js').changePage3const network = require('./js/network.js')4const blockchain = require('./js/blockchain.js')56document.onreadystatechange = function () {7 if (document.readyState == 'complete') {8 const window = remote.getCurrentWindow()9 // Close Buttons10 document.getElementById('min').addEventListener('click',() => {11 window.minimize()12 })13 document.getElementById('max').addEventListener('click',() => {14 if (window.isMaximized()) {15 window.unmaximize()16 } else {17 window.maximize()18 }19 })20 document.getElementById('close').addEventListener('click',() => {21 window.close()22 })2324 // Changing pages2526 // this opens the initial page27 changePage('overview')2829 // event listeners for each button30 // there is probably a better way of doing this31 document.getElementById('overview').addEventListener('click',() => {32 changePage('overview')33 })34 document.getElementById('make').addEventListener('click',() => {35 changePage('make')36 })37 document.getElementById('wallets').addEventListener('click',() => {38 changePage('wallets')39 })40 document.getElementById('history').addEventListener('click',() => {41 changePage('history')42 })43 document.getElementById('view').addEventListener('click',() => {44 changePage('view')45 })46 document.getElementById('mine').addEventListener('click',() => {47 changePage('mine')48 })49 document.getElementById('network-settings').addEventListener('click',() => {50 changePage('network-settings')51 })52 document.getElementById('app-settings').addEventListener('click',() => {53 changePage('app-settings')54 })55 document.getElementById('dev').addEventListener('click',() => {56 // toggle dev tools57 var webcontents = remote.getCurrentWebContents()58 if (webcontents.isDevToolsOpened()) {59 webcontents.closeDevTools()60 } else {61 webcontents.openDevTools()62 }63 })6465 // start server66 network.init()67 }68}index.html:
xxxxxxxxxx421<html>2 <head>3 <meta charset="utf-8">4 <title>Arbitra Client</title>5 <link rel="stylesheet" href="static/style.css"/>6 <script defer src="renderer.js" type="text/javascript"></script>7 <script defer src="static/fontawesome.min.js"></script>8 </head>9 <body>10 <div class="left">11 <h1 class="money" id="current-balance">0</h1>12 <ul>13 <li><i class="fa fa-fw fa-rss" aria-hidden="true"></i> <span id="connections">0</span> connections</li>14 <li><i class="fa fa-fw fa-chain" aria-hidden="true"></i> <span id="height">0</span> blocks in blockchain</li>15 </ul>16 <div class="items" id="nonodes">WARNING: No connections</div>17 <div class="subsec link" id="overview">Overview</div>18 <div class="subsec">Transactions</div>19 <div class="items link" id="make">Make Transactions</div>20 <div class="items link" id="history">Transaction History</div>21 <div class="items link" id="wallets">Wallets</div>22 <div class="subsec">Blockchain</div>23 <div class="items link" id="mine">Mine for Arbitrary Units</div>24 <div class="items link" id="view">View Blockchain</div>25 <div class="subsec">Settings</div>26 <div class="items link" id="network-settings">Network Settings</div>27 <div class="items link" id="app-settings">Application Settings</div>28 <div class="subsec link" id="dev">Toggle Dev Tools</div>29 </div>30 <div class="right">31 <div class="dragbar"></div>32 <div class="closebox">33 <i id="min" class="fa fa-window-minimize" aria-hidden="true"></i>34 <i id="max" class="fa fa-window-restore" aria-hidden="true"></i>35 <i id="close" class="fa fa-window-close" aria-hidden="true"></i>36 </div>37 <div id="body">38 <img src="static/au-icon.png" alt="splash image">39 </div>40 </div>41 </body>42</html>fontawesome.min.js:
Font awesome v4.7.0 is availible to download here:
https://fontawesome.com/v4.7.0/
au-icon.png:

style.css:
xxxxxxxxxx2681* {2 font-family: Segoe UI, Helvetica, sans-serif;3 margin: 0;4 transition: background-color 0.5s ease;5 transition: color 0.2s ease;6}78body {9 height: 100vh;10 overflow: auto;11}1213h1 {14 margin-bottom: 8px;15}1617p {18 margin-top: 2px;19 margin-bottom: 5px;20}2122button, input[type=submit] {23 background-color: #fdfdfd;24 font-weight: 400;25 margin: 2px 0;26 min-width: 70px;27 padding: 4px 10px;28 border: 1px solid #333;29 border-radius: 5px;30}3132button:hover, input[type=submit]:hover {33 background-color: #333;34 color: #fdfdfd;35}3637input[type=text], input[type=number] {38 background-color: #fdfdfd;39 font-weight: 400;40 margin: 2px 0;41 width: calc(100% - 20px);42 max-width: 300px;43 padding: 4px 10px;44 border: 1px solid #333;45 border-radius: 5px;46 outline: none;47}4849input[type=text]:focus, input[type=number]:focus {50 border-radius: 5px;51}5253select {54 background-color: #fdfdfd;55 font-weight: 400;56 margin: 2px 0;57 width: calc(100% - 20px);58 max-width: 300px;59 padding: 3px 5px;60 border: 1px solid #333;61 border-radius: 5px;62 outline: none;63}6465.highlight {66 background-color: #333;67 display: inline;68 color: #fdfdfd;69 border-radius: 3px;70 padding: 1px 3px 3px; 71}7273a {74 text-decoration: none;75}7677p > a {78 color: #666;79}8081p > a:hover {82 color: #fdfdfd;83 background-color: #333;84 padding: 0 2px 2px;85 margin: 0 -2px -2px;86 text-decoration: none;87 border-radius: 3px;88}8990#console {91 box-sizing: border-box;92 width: 100%;93 height: calc(100% - 180px);94 min-height: 300px;95 background-color: #ececec;96 border-radius: 5px;97 border: 1px solid #333;98 padding: 5px;99 overflow-y: auto;100 overflow-x: hidden;101 font-family: 'Courier New', 'Courier', monospace;102}103104.money::after {105 content: "au";106 font-size: 0.65em;107}108109.left {110 width: 300px;111 min-height: calc(100% - 20px);112 float: left;113 padding-top: 20px;114 background-color: #333;115 /*background-image: url(https://i.redd.it/xqiqn0pd5ud01.png);116 background-position: center;117 background-repeat: no-repeat;118 box-shadow: inset -10px 0 10px -10px #333;*/119}120121.left > * {122 color: #fdfdfd;123 list-style: none;124 padding-left: 25px;125 /*text-shadow: 1px 1px 1px #333;*/126}127128.link:hover {129 background-color: rgba(43, 43, 43, 0.5);130 cursor: pointer;131}132133.left > h1 {134 font-size: 2.4em;135}136137.subsec {138 font-size: 1.5em;139 padding: 3px;140 padding-left: 25px;141 margin-top: 15px;142 cursor: default;143}144145.items {146 font-size: 1.2em;147 padding: 3px;148 padding-left: 35px;149}150151.right {152 width: calc(100vw - 300px);153 height: 100vh;154 overflow-y: auto;155 background-color: #fdfdfd;156 position: fixed;157 top: 0;158 right: 0;159}160161.dragbar {162 width: calc(100% - 150px);163 height: 50px;164 float: left;165 app-region: drag;166}167168.closebox {169 width: 140px;170 height: 40px;171 float: left;172 padding: 5px;173}174175.closebox > i {176 font-size: 2em;177 padding-left: 10px;178 color: #333;179 cursor: pointer;180}181182#close:hover {183 color: red;184}185186#body {187 width: calc(100% - 20px);188 padding: 10px;189 color: #333;190}191192#body > h1 {193 font-size: 2.2em;194 border-bottom: 2px solid #333;195}196197#body > img {198 margin-top: calc(50vh - 100px);199 margin-left: calc(50% - 50px);200}201202.td {203 max-width: 100px;204 overflow-x: scroll;205}206207.highlight-box {208 margin-top: 20px;209 width: calc(100% - 20px);210 position: relative;211 border-radius: 5px;212 padding: 10px 10px 5px;213 background-color: #ececec;214 border: 1px solid #333;215}216217.highlight-box > button {218 position: absolute;219 top: 5px;220 right: 10px;221}222223.list {224 overflow-y: auto;225 overflow-x: hidden;226}227228.list-item {229 overflow-x: auto;230 width: calc(100% - 12px);231 background-color: white;232 margin: 5px 0;233 padding: 5px;234 border: 1px solid #333;235 border-radius: 5px;236 white-space: nowrap;237}238239.hidden {240 display: none;241}242243#error {244 color: red;245}246247#nonodes {248 padding-left: 25px;249 margin-top: 10px;250 margin-bottom: -10px;251}252253::scrollbar {254 width: 8px;255 height: 8px;256}257258::scrollbar-track {259 background: #f1f1f1; 260}261262::scrollbar-thumb {263 background: #888; 264}265266::scrollbar-thumb:hover {267 background: #333; 268}app-settings.html:
xxxxxxxxxx141<h1>Settings</h1>23<h2>App Settings</h2>45<p>Application Version: <span id="version"></span></p>67<h3>Save wallets</h3>8<p>Save wallets.json to somewhere in your computer.</p>9<button id="save">Save</button>1011<h3>Clear cached file</h3>12<p>Warning - this will delete any pending transactions, and you will have to redownload the blockchain. It will not delete your wallets.</p>13<button id="clear">Clear cache</button>14<p id="ca-save" class="hidden">Cache cleared</p>history.html:
xxxxxxxxxx91<h1>Transactions</h1>23<h2>Transaction History</h2>45<div class="highlight-box">6 <h3>Recent Transactions</h3>7 <button id="create">Make transaction</button>8 <div class="list" id="tx-list"></div>9</div>make.html:
xxxxxxxxxx181<h1>Transactions</h1>23<h2>Make Transactions</h2>45<div class="hidden" id="error">6 <p><b>Error:</b> missing/incorrect form values</p>7</div>89<form>10 <p>To:</p>11 <input type="text" id="to" placeholder="Address"><br>12 <p>From:</p>13 <div id="inputs">14 </div>15 <button type="button" id="addInput">Add input</button>16 <p>Please note that each wallet can only be used once</p>17 <button type="button" id="send">Send</button>18</form>mine.html:
xxxxxxxxxx71<h1>Blockchain</h1>23<h2>Mine for Arbitrary Units</h2>45<button id="toggle" style="margin-right:5px">Start</button><button id="clear">Clear</button> <b>Please note:</b> Mining is very CPU intensive67<pre id="console"></pre>network-settings.html:
xxxxxxxxxx301<h1>Settings</h1>23<h2>Network Settings</h2>45<h3>Add node</h3>6<p>Enter an IP address, and it will attempt to connect.</p>7<input type="text" id="sendto"/>8<button id="send">Send ping</button>9<p id="pg-save" class="hidden">Ping sent</p>1011<h3>Target connections</h3>12<p>The target number of connections that the client will try to get. Current: <span id="curr"></span></p>13<input type="number" id="target"/>14<button id="target-save">Save</button>15<p id="min-save" class="hidden">Option saved</p>1617<h3>Advertise</h3>18<p>Sometimes, nodes will ask others to send a list of clients that they are in contact with. Your IP will only be shared if this setting is turned on.</p>19<select id="advertise">20 <option value="true">On</option>21 <option value="false">Off</option>22</select>23<button id="save">Save</button>24<p id="ad-save" class="hidden">Option saved</p>252627<h3>Refresh connections</h3>28<p>Refresh this client's connections to other nodes.</p>29<button id="refresh">Refresh</button>30<p id="re-save" class="hidden">Connections refreshed</p>wallets.html:
xxxxxxxxxx91<h1>Transactions</h1>23<h2>Wallets</h2>45<div class="highlight-box">6 <h3>My wallets</h3>7 <button id="create">Create new wallet</button>8 <div class="list" id="wallet-list"></div>9</div>wallets-create.html:
xxxxxxxxxx111<h1>Transactions</h1>23<h2>Create a Wallet</h2>45<p>Wallet Name:</p>6<input type="text" id="name" placeholder="My Wallet"><br>7<p>Public Key:</p>8<div class="list-item" id="public"></div>9<p>Private Key (DO NOT SHARE!):</p>10<div class="list-item" id="private"></div>11<button id="create">Create Wallet</button>overview.html:
xxxxxxxxxx161<h1>Arbitra</h1>23<p>Welcome to Arbitra!</p>45<br>67<h3>You can:</h3>89<p><i class="fa fa-fw fa-envelope" aria-hidden="true"></i> Send a transaction</p>10<p><i class="fa fa-fw fa-chain" aria-hidden="true"></i> Mine the blockchain for Arbitrary Units</p>11<p><i class="fa fa-fw fa-eye" aria-hidden="true"></i> View past transactions and the blockchain</p>12<p><i class="fa fa-fw fa-rss" aria-hidden="true"></i> Connect to other nodes on the network</p>1314<br>1516<p>Got any questions/feedback? Email me at <a href="mailto:hello@samuelnewman.uk">hello@samuelnewman.uk</a></p>parse.js:
x
1const network = require('./network.js')2const hash = require('./hashing.js')3const ecdsa = require('./ecdsa.js')4const blockchain = require('./blockchain.js')5const file = require('./file.js')67function transaction(tx) {8 var from = tx.from9 var len = from.length10 var input11 var concat12 var repeats = []13 // goes through the transaction inputs14 // and checks that they're all valid15 for (var i; i < len; ++i) {16 input = from[i]17 if (repeats.contains(input)) {18 // wallets in a transaction must be unique19 throw 'parse'20 }21 // this is the "message" for the ecdsa function22 concat = input.amount+tx.to+tx.time23 ecdsa.verifyMsg(concat,input.signature,input.person,(result) => {24 if (result) {25 blockchain.checkBalance(input.person,input.amount,(balanceCheck) => {26 if (balanceCheck) {27 repeats.push(input)28 } else {29 throw 'amount'30 }31 })32 } else {33 throw 'signature'34 }35 })36 }37}3839function block(body) {40 const difficulty = 641 var txlist = body.transactions42 var len = txlist.length43 var tx44 var blockhash = hash.sha256hex(JSON.stringify(body))45 // verify all the transactions46 var pass = true47 for (var i = 0; i < body.difficulty; i++) {48 if (blockhash.charAt(i) !== 'a') {49 pass = false50 }51 }52 if (body.difficulty === difficulty && pass) {53 for (var i; i < len; ++i) {54 tx = txlist[i]55 try {56 transaction(tx)57 } catch(e) {58 if (e === 'signature' || e === 'amount') {59 throw 'transaction'60 }61 }62 }63 } else {64 throw 'difficulty'65 }66}6768function chain(chain) {69 try {70 for(var hash in chain) {71 block(chain[hash])72 }73 } catch(e) {74 console.warn('Received chain invalid')75 throw e76 }77}7879function tx(msg,callback) {80 var reply = {81 "header": {82 "type": "ok"83 },84 "body": {}85 }86 // verify that it works87 transaction(msg.body)88 // add to txpool89 file.getAll('txpool',(data) => {90 var txpool = JSON.parse(data)91 if (!txpool.includes(msg.body)) {92 file.append('txpool',msg.body,() => {93 // send to contacts94 sendToAll(msg)95 // reply96 callback(reply)97 })98 }99 },'[]')100}101102function bk(msg,callback) {103 var reply = {104 "header": {105 "type": "ok"106 },107 "body": {}108 }109 block(msg.body)110 // if nothing has been thrown, add to local blockchain111 blockchain.addBlock(msg)112 network.sendToAll(msg)113 callback(reply)114}115116function hr(msg,callback) {117 file.getAll('blockchain',(data) => {118 if (data === null || data === "{}") {119 throw 'notfound'120 } else {121 blockchain.getTopBlock(JSON.parse(data),(top) => {122 var reply = {123 "header": {124 "type": "bh"125 },126 "body": {127 "hash": top128 }129 }130 callback(reply)131 })132 }133 })134}135136function pg(msg,ip,callback) {137 // store the connection138 pgreply(msg,ip)139 // send a reply140 file.get('advertise','network-settings',(data) => {141 if (data === 'true' || data === 'false') {142 var advertise = data143 } else {144 var advertise = 'true'145 }146 var reply = {147 "header": {148 "type": "pg"149 },150 "body": {151 "advertise": advertise152 }153 }154 callback(reply)155 })156}157158function pgreply(msg,ip) {159 var store = {}160 store['ip'] = ip161 store['advertise'] = msg.body.advertise162 file.getAll('connections',(data) => {163 var repeat = false164 // checks to see if the ip is already in connections.json165 nodes = JSON.parse(data)166 nodes.forEach((node) => {167 if (node.ip === ip) {168 repeat = true169 }170 })171 // stores it if not and if it is not our ip172 const ourip = require('ip').address173 if (!repeat && ip !== ourip()) {174 file.append('connections',store,() => {175 console.log('Connection added: '+ip)176 document.getElementById('nonodes').classList.add('hidden')177 var current = document.getElementById('connections').textContent178 document.getElementById('connections').textContent = parseInt(current) + 1179 })180 }181 },'[]') // if it fails it returns an empty array182}183184function bh(msg,callback) {185 file.getAll('blockchain',(data) => {186 var mainchain = JSON.parse(data)187 if (!Object.keys(mainchain).includes(msg.body.hash)) {188 // if the received top hash is not equal to the one on disk189 // and it's not in the blockchain, then send out a chain request190 var chainrequest = {191 "header": {192 "type": "cr"193 },194 "body": {195 "hash": msg.body.hash196 }197 }198 network.sendToAll(chainrequest)199 }200 },'{}')201}202203function nr(msg,ip,callback) {204 var max = Infinity205 if (msg.hasOwnProperty('max')) {206 var max = msg.max207 }208 var nodes = []209 file.getAll('connections',(data) => {210 if (data === null) {211 throw 'notfound'212 }213 var connections = JSON.parse(data)214 connections.forEach((connection,i) => {215 if (connection.ip !== ip && i < max && connection.advertise === "true") {216 nodes.push(connections)217 }218 })219 var reply = {220 "header": {221 "type": "nd"222 },223 "body": {224 "nodes": nodes225 }226 }227 callback(reply)228 })229}230231function nd(msg) {232 // some nodes we can connect to233 file.get('advertise','network-settings',(data) => {234 if (data === 'true' || data === 'false') {235 var advertise = data236 } else {237 var advertise = 'true'238 }239 var ping = {240 "header": {241 "type": "pg"242 },243 "body": {244 "advertise": advertise245 }246 }247 file.getAll('connections',(data) => {248 // this must get connection data, as otherwise it wouldn't have received this message249 var connections = JSON.parse(data)250 msg.body.nodes.forEach((node) => {251 var send = true252 // if we are already connected to the node don't send253 connections.forEach((connection) => {254 if (node.ip === connection) {255 send = false256 }257 })258 // otherwise send a ping259 if (send) {260 network.sendMsg(ping,node.ip)261 }262 })263 })264 })265}266267function cr(msg,callback) {268 if (msg.body.hasOwnProperty('hash')) {269 blockchain.get(msg.body.hash,(block) => {270 if (block === null) {271 throw 'notfound'272 } else {273 blockchain.getChain(msg.body.hash,(chain) => {274 if (chain === null) {275 throw 'notfound'276 } else {277 var reply = {278 "header": {279 "type": "cn"280 },281 "body": {282 "chain": chain283 }284 }285 callback(reply)286 }287 })288 }289 })290 } else {291 blockchain.mainChain((chain) => {292 var reply = {293 "header": {294 "type": "cn"295 },296 "body": {297 "chain": chain298 }299 }300 callback(reply)301 })302 }303}304305function cn(msg) {306 for (var key in msg.chain) {307 // an oversight means we need to give it msg.body308 var block = {"body":msg.chain[key]}309 blockchain.addBlock(block)310 }311}312313function er(msg) {314 file.append('error-logs',msg)315}316317318exports.tx = tx319exports.bk = bk320exports.hr = hr321exports.nr = nr322exports.pg = pg323exports.pgreply = pgreply324exports.nd = nd325exports.bh = bh326exports.cr = cr327exports.er = er328exports.cn = cn329exports.block = block330exports.transaction = transactionnetwork.js:
xxxxxxxxxx3121const net = require('net')2const hash = require('./hashing.js')3const file = require('./file.js')4const parse = require('./parse.js')5const version = require('../package.json').version6const blockchain = require('./blockchain.js')78const port = 2018910function init() {11 // creates a server that will receive all the messages12 // when it receives data, it will pass it to parseMsg()13 // and reply with whatever it sends back14 var server = net.createServer((socket) => {15 var ip = socket.remoteAddress16 socket.setEncoding('utf8')17 // when it receives data, send to parseMsg()18 socket.on('data',(data) => {19 console.log('Received connection from: '+ip)20 console.log('Server received: '+data)21 parseMsg(data,ip,(msg) => {22 if (msg.header.type !== 'tx' && msg.header.type !== 'bk') {23 msg.body['time'] = Date.now()24 msg.header['version'] = version25 msg.header['size'] = Buffer.byteLength(JSON.stringify(msg.body))26 msg.header['hash'] = hash.sha256hex(JSON.stringify(msg.body))27 }28 var reply = JSON.stringify(msg)29 console.info('Sending message to '+ip+': '+reply)30 socket.write(reply)31 file.append('sent',msg.header.hash)32 socket.end()33 })34 })35 })36 37 // server listens on this port38 // should be 201839 server.listen(port,'0.0.0.0',() => { 40 console.log('Server listening on port',port)41 })4243 // wipe connections44 // this will be populated with connections that succeed45 file.storeAll('connections',[])46 // inital connection attempt as setInterval runs at then end of the 60 seconds47 connect()48 49 try {50 blockchain.calcBalances()51 } catch(e) {52 console.warn('calcBalances() failed')53 }5455 // this is a loop that maintains connections and56 // sends top hash requests to make sure the client is up to date57 // it goes on forever, every minute58 setInterval(() => {59 console.log('Interval')60 var connections = parseInt(document.getElementById('connections').textContent)61 // first check that we have enough connections62 file.get('target-connections','network-settings',(target) => {63 // if the current number of of connections is less than the minimum64 // as defined by user settings, connect65 if (connections < target) {66 connect()67 // if it's still not enough after 15 seconds, send node requests68 setTimeout(() => {69 connections = parseInt(document.getElementById('connections').textContent)70 if (connections < target) {71 var nr = {72 "header": {73 "type": "nr"74 },75 "body": {}76 }77 sendToAll(nr)78 }79 },15000)80 } else {81 // save current connections to recent connections82 file.getAll('connections',(data) => {83 if (connections !== null) {84 file.storeAll('recent-connections',JSON.parse(data))85 }86 })87 }88 connections = parseInt(document.getElementById('connections').textContent)89 if (connections === 0) {90 document.getElementById('nonodes').classList.remove('hidden')91 } else {92 // check that the chain is up to date93 var hr = {94 "header": {95 "type": "hr"96 },97 "body": {}98 }99 sendToAll(hr)100 }101 },5) // if it fails to open the file it sets target to five102 },60000)103}104105function connect() {106 // try to connect to other nodes through old connections107 file.getAll('recent-connections',(data) => {108 var connections = JSON.parse(data)109 file.get('advertise','network-settings',(data) => {110 if (data === 'true' || data === 'false') {111 var advertise = data112 } else {113 var advertise = 'true'114 }115 var ping = {116 "header": {117 "type": "pg"118 },119 "body": {120 "advertise": advertise121 }122 }123 file.getAll('connections',(curdata) => {124 var current = JSON.parse(curdata)125 connections.forEach((node) => {126 if (!current.includes(node)) {127 sendMsg(ping,node.ip)128 }129 })130 },'[]')131 132 // wait ten seconds to see if any connections have been made133 setTimeout(() => {134 // get the number of connections from textContent135 var connectCount = parseInt(document.getElementById('connections').textContent)136 if (connectCount === 0) {137 console.warn('No connections found!')138 document.getElementById('nonodes').classList.remove('hidden')139 console.warn('Connecting to backup server')140 // wavecalcs.com is friend's server, and should be online for the purposes of this project141 // wavecalcs.com = 5.81.186.90142 sendMsg(ping,'5.81.186.90')143 }144 },10000)145 })146 },'[]')147}148149function sendMsg(msg,ip,callback) {150 // for checking that the message hasn't already been sent151 file.getAll('sent',(data) => {152 // for some reason, sent.json sometimes ends with [...]]153 // until I find the source of the bug, this will do154 if (data[data.length-1] === data[data.length-2]) {155 data = data.slice(0,-1)156 }157 var sent = JSON.parse(data)158 if (msg.header.type !== 'bk' && msg.header.type !== 'tx') {159 // don't want to affect the body of a block160 // and the time of the tx is crucial as well161 // as it will throw off the hash162 msg.body['time'] = Date.now()163 }164 msg.header['version'] = version165 msg.header['size'] = Buffer.byteLength(JSON.stringify(msg.body))166 msg.header['hash'] = hash.sha256hex(JSON.stringify(msg.body))167168 // check that the message hasn't already been sent169 if (!sent.includes(msg.header.hash)) {170 var sendMe = JSON.stringify(msg)171 console.info('Sending message to '+ip+': '+sendMe)172173 // actually go send the message174 var client = new net.Socket()175 client.connect(port,ip,() => {176 client.write(sendMe)177 // add the hash to the sent messages file178 file.append('sent',msg.header.hash)179 client.on('data',(data) => {180 console.log('Client received: '+data)181 parseReply(data,ip,() => {182 client.destroy()183 })184 })185 client.on('close',() => {186 })187 client.on('timeout',() => {188 console.warn('Client timed out')189 client.destroy()190 })191 client.on('error',(e) => {192 console.warn('Client timed out')193 client.destroy()194 })195 })196 } else {197 console.log('Message already sent')198 }199 },'[]')200}201202function parseMsg(data,ip,callback) {203 // parse incoming messages and replies204 // by calling parse functions205 try {206 var msg = JSON.parse(data)207 if (msg.header.hash === hash.sha256hex(JSON.stringify(msg.body))) {208 if (msg.header.type === 'tx') {209 // transaction210 // callback is used to send the reply211 parse.tx(msg,callback)212 } else if (msg.header.type === 'bk') {213 // block214 parse.bk(msg,callback)215 } else if (msg.header.type === 'hr') {216 // hash request217 parse.hr(msg,callback)218 } else if (msg.header.type === 'cr') {219 // chain request220 parse.cr(msg,callback)221 } else if (msg.header.type === 'pg') {222 // ping223 parse.pg(msg,ip,callback)224 } else if (msg.header.type === 'nr',callback) {225 // node request226 parse.nr(msg,ip,callback)227 } else {228 throw 'type'229 }230 } else {231 throw 'hash'232 }233 } catch(e) {234 // catching any errors and replying with an error message235 console.warn(e)236 var error237 if (e.name === 'SyntaxError') {238 error = 'parse'239 } else {240 error = e241 }242 var reply = {243 "header": {244 "type": "er"245 },246 "body": {247 "error": error248 }249 }250 file.append('error-logs',data)251 callback(reply)252 }253}254255function parseReply(data,ip,callback=()=>{}) {256 // parse incoming replies257 // by calling parse functions258 try {259 var msg = JSON.parse(data)260 if (msg.header.hash == hash.sha256hex(JSON.stringify(msg.body))) {261 if (msg.header.type === 'cn') {262 // chain263 parse.cn(msg)264 } else if (msg.header.type === 'bh') {265 // top hash266 parse.bh(msg)267 } else if (msg.header.type === 'nd') {268 // nodes269 parse.nd(msg)270 } else if (msg.header.type === 'pg') {271 // ping272 parse.pgreply(msg,ip)273 } else if (msg.header.type === 'ok') {274 // message received ok275 console.info('message recieved ok')276 } else if (msg.header.type === 'er') {277 // error (uh oh)278 console.warn('We recieved an error')279 parse.er(msg)280 } else {281 throw 'type'282 }283284 } else {285 throw 'hash'286 }287 } catch(e) {288 console.warn('Reply error: '+e)289 file.append('error-log',msg)290 } finally {291 // call the callback, if needed292 callback()293 }294}295296function sendToAll(msg) {297 file.getAll('connections',(data) => {298 // doesn't do anything if there's no connections299 if (data !== null || data === '' || data === '[]') {300 nodes = JSON.parse(data)301 // go through connections and send a message to each302 nodes.forEach((node) => {303 sendMsg(msg,node.ip)304 })305 }306 })307}308309exports.init = init310exports.sendMsg = sendMsg311exports.sendToAll = sendToAll312exports.connect = connectmining-script.js:
xxxxxxxxxx1991const hash = require(__dirname+'/js/hashing.js')2const fs = require('fs')34class Miner {5 constructor(path) {6 const difficulty = 67 this.path = path8 // this is for the printing later9 this.hashes = 010 this.dhash = 011 this.t1 = Date.now()12 this.t2 = Date.now()13 this.tt = Date.now()14 // difficulty is static15 this.block = {16 "header": {17 "type": "bk"18 },19 "body": {20 "difficulty": difficulty21 }22 }2324 var transactions = JSON.parse(fs.readFileSync(this.path+'txpool.json','utf-8'))25 this.block.body['transactions'] = transactions2627 // parent and height28 var top = this.getTopBlock()29 if (top === null) {30 this.block.body['parent'] = '0000000000000000000000000000000000000000000000000000000000000000'31 this.block.body['height'] = 032 } else {33 var blockchain = JSON.parse(fs.readFileSync(this.path+'blockchain.json','utf8'))34 this.block.body['parent'] = top35 this.block.body['height'] = blockchain[top].height+136 }37 // miner38 var wallets = JSON.parse(fs.readFileSync(this.path+'wallets.json','utf-8'))39 var miner = wallets[0].public40 this.block.body['miner'] = miner4142 postMessage('Block formed, mining initiated')43 }4445 mine() {46 // repeatedly hashes with a random nonce47 while (true) {48 this.rand((nonce) => {49 this.block.body['nonce'] = nonce50 // t2 is updated every loop51 this.block.body['time'] = this.t252 this.hashBlock(this.block.body,(hash) => {53 this.hashes++54 this.dhash++55 // checks difficulty56 var pass = true57 for (var i = 0; i < this.block.body.difficulty; i++) {58 if (hash.charAt(i) !== 'a') {59 pass = false60 }61 }62 this.t2 = Date.now()63 // this triggers if the block has passed the difficulty test64 if (pass) {65 postMessage('Hash found! Nonce: '+nonce)66 postMessage(hash)67 postMessage(this.block)68 // get rid of the pending transactions69 fs.writeFileSync(this.path+'txpool.json','[]','utf-8')70 // set the new block things71 this.block.body.transactions = []72 var top = this.getTopBlock()73 this.block.body['parent'] = hash74 this.block.body['height'] += 1 75 } else {76 // printing for the console77 if ((this.t2-this.t1) > 10000) {78 // calculate hashes per second (maybe)79 // *1000 turns it into seconds80 var hs = (this.dhash/(this.t2-this.t1))*100081 this.dhash = 082 this.t1 = Date.now()83 postMessage('Hashing at '+hs.toFixed(3)+' hashes/sec - '+this.hashes+' hashes in '+Math.floor((this.t1-this.tt)/1000)+' seconds')8485 // check to see if the block has updated86 fs.readFile(this.path+'txpool.json','utf-8',(err,content) => {87 if (err) {88 // if the file doesn't exist, set content to []89 if (err.code === 'ENOENT') {90 content = '[]'91 } else {92 postMessage('Error opening file')93 throw err94 }95 }96 var current = JSON.stringify(this.block.body.transactions)97 // change the transactions if they are different98 if (current !== content) {99 var newtx = JSON.parse(content)100 this.block.body['transactions'] = newtx101 postMessage('Transactions updated')102 }103 })104 }105 }106 })107 })108 }109 }110111 rand(callback) {112 callback(Math.floor(10000000000000000*Math.random()))113 }114 115 hashBlock(block,callback) {116 var hashed = hash.sha256hex(JSON.stringify(block))117 callback(hashed)118 }119120 getTopBlock() {121 const genesis = '0000000000000000000000000000000000000000000000000000000000000000'122 try {123 var data = fs.readFileSync(this.path+'blockchain.json','utf8')124 } catch(e) {125 return null126 }127 if (data === '{}' || data === '') {128 return null129 }130 var fullchain = JSON.parse(data)131 // get the origin block132 // as there is nothing under it to be wrong133 for (var best in fullchain) {134 if (fullchain[best].parent === genesis) {135 break136 }137 }138 if (typeof best !== 'undefined' && fullchain[best].parent === genesis) {139 // iterates through the fullchain140 for (var key in fullchain) {141 // larger height the better142 if (fullchain[key].height > fullchain[best].height) {143 var candidate = true144 // iterate down the chain to see if you can reach the bottom145 // if the parent is undefined at any point it is not part of the main chain146 // run out of time for a more efficient method147 var current = key148 var parent149 while (fullchain[current].parent !== genesis) {150 parent = fullchain[current].parent151 if (typeof fullchain[parent] !== 'undefined') {152 current = parent153 } else {154 candiate = false155 }156 }157 if (candidate) {158 best = key159 }160 // otherwise, if they're the same pick the oldest one161 } else if (fullchain[key].height === fullchain[best].height) {162 if (fullchain[key].time < fullchain[best].time) {163 // see other comments164 var candidate = true165 var current = key166 while (fullchain[current].parent !== genesis) {167 parent = fullchain[current].parent168 if (typeof fullchain[parent] !== 'undefined') {169 current = parent170 } else {171 candiate = false172 }173 }174 if (candidate) {175 best = key176 }177 }178 }179 }180 } else {181 best = null182 }183 return best184 }185}186187onmessage = (path) => {188 postMessage('Path recieved')189 try {190 var miner = new Miner(path.data)191 miner.mine()192 } catch(e) {193 postMessage('Error caught')194 if (typeof e !== 'string') {195 e = e.message196 }197 postMessage(e)198 }199}file.js:
xxxxxxxxxx1401const remote = require('electron').remote2const fs = require('fs')34function store(key,data,file,callback=()=>{}) {5 // put data in file6 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'7 fs.readFile(path,'utf-8',(err,content) => {8 if (err) {9 // if the file doesn't exist, it creates an empty object literal10 // it will then continue on and create the file later11 if (err.code === 'ENOENT') {12 content = '{}'13 } else {14 alert('Error opening '+file+'.json')15 throw err16 }17 }18 // try to parse content to js then push the data19 try {20 var jsondata = JSON.parse(content)21 if (jsondata.hasOwnProperty(key) && Array.isArray(data)) {22 // if the key exists it concatenates the two arrays, creates a new set23 // which removes duplicates, then turns it back to an array24 // https://gist.github.com/telekosmos/3b62a31a5c43f40849bb#gistcomment-182680925 var set = new Set(jsondata[key].concat(data))26 jsondata[key] = Array.from(set)27 } else {28 // otherwise sets the key to the data29 jsondata[key] = data30 }31 } catch(e) {32 console.warn(e)33 var jsondata = {}34 jsondata[key] = data35 } finally {36 // writes the contents back to the file37 // or makes the file if it doesn't exist yet38 content = JSON.stringify(jsondata)39 fs.writeFile(path,content,'utf-8',(err) => {40 if (err) throw err41 callback()42 })43 }44 })45}4647function get(key,file,callback,fail=null) {48 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'49 fs.readFile(path,'utf-8',(err,content) => {50 if (err) {51 // if the file doesn't exist, return null52 if (err.code === 'ENOENT') {53 console.warn(file+'.json not found')54 console.trace()55 callback(fail)56 return57 } else {58 alert('Error opening '+file+'.json')59 throw err60 }61 }62 // try to parse content to js then push the data63 try {64 var jsondata = JSON.parse(content)65 var result = jsondata[key]66 } catch(e) {67 // if the key doesn't exist, return null68 console.warn(e)69 var result = fail70 } finally {71 callback(result)72 }73 })74}7576function getAll(file,callback,fail=null) {77 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'78 fs.readFile(path,'utf-8',(err,content) => {79 if (err) {80 // if the file doesn't exist, return null81 if (err.code === 'ENOENT') {82 console.warn(file+'.json not found')83 content = fail84 } else {85 alert('Error opening '+file+'.json')86 console.error('Error opening '+file+'.json')87 throw err88 }89 }90 callback(content)91 })92}9394function storeAll(file,data,callback=()=>{}) {95 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'96 content = JSON.stringify(data)97 fs.writeFile(path,content,'utf-8',(err) => {98 if (err) throw err99 callback()100 })101}102103function append(file,data,callback=()=>{}) {104 // write data to a file, but where the file is an array so no key105 var path = remote.app.getPath('appData')+'/arbitra-client/'+file+'.json'106 fs.readFile(path,'utf-8',(err,content) => {107 if (err) {108 // if the file doesn't exist, it creates an empty object literal109 // it will then continue on and create the file later110 if (err.code === 'ENOENT') {111 content = '[]'112 } else {113 alert('Error opening '+file+'.json')114 throw err115 }116 }117 // try to parse content to js then push the data118 try {119 var jsondata = JSON.parse(content)120 jsondata.push(data)121 } catch(e) {122 console.warn(e)123 var jsondata = [data]124 } finally {125 // writes the contents back to the file126 // or makes the file if it doesn't exist yet127 content = JSON.stringify(jsondata)128 fs.writeFile(path,content,'utf-8',(err) => {129 if (err) throw err130 callback()131 })132 }133 })134}135136exports.store = store137exports.get = get138exports.getAll = getAll139exports.append = append140exports.storeAll = storeAllecdsa.js:
xxxxxxxxxx1631const crypto = require('crypto')2const bigInt = require('big-integer')3const hash = require('./hashing.js')45// elliptic curve secp256k16const curve = {7 a: bigInt('0'),8 b: bigInt('7'),9 p: bigInt('115792089237316195423570985008687907853269984665640564039457584007908834671663'),10 g: {11 x: bigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),12 y: bigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424')13 },14 n: bigInt('115792089237316195423570985008687907852837564279074904382605163141518161494337')15}1617function randomNum(min=1,max=curve.n) {18 var randomValue = max.add(1)19 while (randomValue.greater(max) || randomValue.lesser(min)) {20 // 32 bytes = 256 bits21 var buffer = crypto.randomBytes(32).toString('hex')22 randomValue = bigInt(buffer,16)23 }24 return randomValue25}2627function onCurve(point) {28 // sees if point is on the curve y^2 = x^3 + ax + b29 if (point !== Infinity) {30 ysq = point.y.square()31 xcu = point.x.pow(3)32 ax = curve.a.multiply(point.x)33 if (ysq.minus(xcu).minus(ax).minus(curve.b).isZero()) {34 throw new Error('not on curve')35 }36 }37}3839function addPoints(P1,P2) {40 onCurve(P1)41 onCurve(P2)42 var m,x,y43 // Point + Infinity = Point44 if (P1 === Infinity) {45 return P246 } else if (P2 === Infinity) {47 return P148 }49 if (P1.x === P2.x) {50 if (P1.y !== P2.y) {51 return Infinity52 } else {53 // finding gradient of tangent54 var t1 = bigInt(3).times(P1.x.square())55 var t2 = bigInt(2).times(P1.y)56 m = bigInt(t1.plus(curve.a)).times(t2.modInv(curve.p))57 }58 } else {59 // finding gradient of line between 2 points60 var t1 = P2.y.minus(P1.y)61 var t2 = P2.x.minus(P1.x)62 m = t1.times(t2.modInv(curve.p))63 }64 // calculating other interception point65 x = bigInt(m.square().minus(P1.x).minus(P2.x)).mod(curve.p)66 y = bigInt(bigInt(m.times(P1.x)).minus(P1.y).minus(m.times(x))).mod(curve.p)67 var P3 = {68 x: x,69 y: y70 }71 onCurve(P3)72 return P373}7475function multiPoints(n,P) {76 if (P === Infinity) {77 return P78 }79 var total = Infinity80 var binary = n.toString(2)81 // reversed binary82 var yranib = binary.split('').reverse()83 // see documentation if confused, it's a bit mathsy84 // to explain in comments85 yranib.forEach(function(bit) {86 if (bit == 1) {87 total = addPoints(total, P)88 }89 P = addPoints(P, P)90 onCurve(P)91 })92 onCurve(total)93 return total94}9596function createKeys(callback) {97 var err98 try {99 var private = randomNum(1, curve.n)100 var public = multiPoints(private, curve.g)101 var x = public.x.toString(16)102 var y = public.y.toString(16)103 public = x+y104 private = private.toString(16)105 } catch (e) {106 err = e107 } finally {108 callback(public, private, err)109 }110}111112function signMsg(msg,private,callback) {113 var err114 var w = bigInt(private,16)115 console.log('Signing: '+msg)116 var z = hash.sha256(msg)117 var r,s118 r = s = bigInt.zero119 while (r.isZero() && s.isZero()) {120 var k = randomNum(1, curve.n)121 try {122 var P = multiPoints(k,curve.g)123 } catch(e) {124 err = e125 callback(0,0,err)126 return127 }128 r = P.x.mod(curve.n)129 s = bigInt(bigInt(w.times(r).plus(z)).times(k.modInv(curve.n))).mod(curve.n)130 }131 var signature = r.toString(16)+s.toString(16)132 callback(signature,err)133}134135function verifyMsg(msg,signature,public,callback) {136 // we need to convert signature and public to the right format137 // q is public key138 var mid = public.length/2139 var q = {140 'x': bigInt(public.slice(0,mid),16),141 'y': bigInt(public.slice(mid),16)142 }143 // r and s is the signature144 var r = bigInt(signature.slice(0,mid),16)145 var s = bigInt(signature.slice(mid),16)146 var result = false147 var z = hash.sha256(msg)148 u1 = bigInt(z.times(s.modInv(curve.n))).mod(curve.n)149 u2 = bigInt(r.times(s.modInv(curve.n))).mod(curve.n)150 try {151 P = addPoints(multiPoints(u1,curve.g),multiPoints(u2,q))152 } catch(e) {153 callback(result)154 return155 }156 result = bigInt(r.mod(curve.n)).equals(P.x.mod(curve.n))157 callback(result)158}159160exports.curve = curve161exports.createKeys = createKeys162exports.signMsg = signMsg163exports.verifyMsg = verifyMsgchangepage.js:
xxxxxxxxxx221const remote = require('electron').remote2const fs = require('fs')34function changePage(name) {5 var path = 'pages/' + name + '.html'6 fs.readFile(path,'utf-8',(err, data) => {7 if (err) {8 alert('An error ocurred reading the file: '+name)9 console.warn('An error ocurred reading the file: '+err.message)10 return11 }12 document.getElementById('body').innerHTML = data13 try {14 const pageJS = require('./pages/'+name+'.js')15 pageJS.init()16 } catch(e) {17 console.error(e)18 }19 })20}2122exports.changePage = changePageblockchain.js:
xxxxxxxxxx2161const file = require('./file.js')2const hash = require('./hashing.js')3const ecdsa = require('./ecdsa.js')4const parse = require('./parse.js')56function getBlock(hash,callback) {7 file.get(hash,'blockchain',callback)8}910function checkBalance(key,amount,callback) {11 file.get(key,'balances',(balance) => {12 // returns true if the wallet's balance is13 // less than or equal to the amount requested14 callback(balance >= amount)15 },0)16}1718function calcBalances() {19 const miningreward = 5000000020 // mainChain gets the longest chain, as only the blocks under the highest21 // actually count22 mainChain((chain) => {23 var balances = {}24 // iterate through the blocks25 for (var key in chain) {26 var block = chain[key]27 transactions = block.transactions28 // iterate through each block to find each transaction29 transactions.forEach((transaction) => {30 // iterate through the inputs31 transaction.from.forEach((from) => {32 // deduct amounts from the inputs33 if (balances.hasOwnProperty(from.wallet)) {34 balances[from.wallet] -= from.amount35 } else {36 balances[from.wallet] = -from.amount37 }38 // add amount to the recipient's balance39 if (balances.hasOwnProperty(transaction.to)) {40 balances[transaction.to] += from.amount41 } else {42 balances[transaction.to] = from.amount43 }44 })45 })46 // mining rewards47 if (balances.hasOwnProperty(block.miner)) {48 balances[block.miner] += miningreward49 } else {50 balances[block.miner] = miningreward51 }52 }53 // calculating the balance in the corner54 file.getAll('wallets',(data) => {55 var wallets = JSON.parse(data)56 var newWallets = []57 var balance = 058 wallets.forEach((wallet) => {59 if (balances.hasOwnProperty(wallet.public)) {60 amount = balances[wallet.public]61 } else {62 amount = 063 }64 // add the au in the wallet to the total balance65 balance += amount66 // and set the balance in the wallet67 newWallets.push({68 "name": wallet.name,69 "public": wallet.public,70 "private": wallet.private,71 "amount": amount72 })73 })74 // change microau to au and set the textcontent of the top left thing75 document.getElementById('current-balance').textContent = balance / 100000076 // save balances77 file.storeAll('wallets',newWallets)78 file.storeAll('balances',balances)79 },'[]')80 })81}8283function addBlock(msg) {84 try {85 parse.block(msg.body)86 // if it failed the test, an error will have been thrown87 file.store(hash.sha256hex(JSON.stringify(msg.body)),msg.body,'blockchain')88 console.log('Block added')89 file.getAll('txpool',(data) => {90 var txpool = JSON.parse(data)91 msg.body.transactions.forEach((tx) => {92 // remove pending transactions if they're in the received block93 txpool.splice(txpool.indexOf(tx),1)94 })95 file.storeAll('txpool',txpool)96 calcBalances()97 },'[]')98 } catch(e) {99 console.warn('Block failed:',JSON.stringify(msg))100 console.warn(e)101 }102}103104function mainChain(callback) {105 var mainchain = {}106 file.getAll('blockchain',(data) => {107 if (data === '{}') {108 callback({})109 } else {110 var fullchain = JSON.parse(data)111 getTopBlock(fullchain,(top) => {112 mainchain[top] = fullchain[top]113 var current = top114 var parent115 while (fullchain[current].parent !== '0000000000000000000000000000000000000000000000000000000000000000') {116 parent = fullchain[current].parent117 mainchain[parent] = fullchain[parent]118 current = parent119 }120 callback(mainchain)121 })122 }123 },'{}')124}125126function getChain(top,callback) {127 var mainchain = {}128 file.getAll('blockchain',(data) => {129 if (data === '{}') {130 callback(null)131 } else {132 try {133 var fullchain = JSON.parse(data)134 mainchain[top] = fullchain[top]135 var current = top136 var parent137 while (fullchain[current].parent !== '0000000000000000000000000000000000000000000000000000000000000000') {138 parent = fullchain[current].parent139 mainchain[parent] = fullchain[parent]140 current = parent141 }142 } catch(e) {143 console.warn(e)144 mainchain = null145 } finally {146 callback(mainchain)147 }148 }149 },'{}')150}151152function getTopBlock(fullchain,callback) {153 const genesis = '0000000000000000000000000000000000000000000000000000000000000000'154 // get the origin block155 // as there is nothing under it to be wrong156 for (var best in fullchain) {157 if (fullchain[best].parent === genesis) {158 break159 }160 }161 if (typeof best !== 'undefined' && fullchain[best].parent === genesis) {162 // iterates through the fullchain163 for (var key in fullchain) {164 // larger height the better165 if (fullchain[key].height > fullchain[best].height) {166 var candidate = true167 // iterate down the chain to see if you can reach the bottom168 // if the parent is undefined at any point it is not part of the main chain169 // run out of time for a more efficient method170 var current = key171 var parent172 while (fullchain[current].parent !== genesis) {173 parent = fullchain[current].parent174 if (typeof fullchain[parent] !== 'undefined') {175 current = parent176 } else {177 candiate = false178 }179 }180 if (candidate) {181 best = key182 }183 // otherwise, if they're the same pick the oldest one184 } else if (fullchain[key].height === fullchain[best].height) {185 if (fullchain[key].time < fullchain[best].time) {186 // see other comments187 var candidate = true188 var current = key189 while (fullchain[current].parent !== genesis) {190 parent = fullchain[current].parent191 if (typeof fullchain[parent] !== 'undefined') {192 current = parent193 } else {194 candiate = false195 }196 }197 if (candidate) {198 best = key199 }200 }201 }202 document.getElementById('height').textContent = fullchain[best].height + 1203 }204 } else {205 best = null206 }207 callback(best)208}209210exports.get = getBlock211exports.checkBalance = checkBalance212exports.calcBalances = calcBalances213exports.addBlock = addBlock214exports.getTopBlock = getTopBlock215exports.mainChain = mainChain216exports.getChain = getChainhashing.js:
xxxxxxxxxx151const crypto = require('crypto')2const bigInt = require('big-integer')34function sha256(data) {5 // creates a sha256 hash, updates it with data, and turns it into a bigint6 var hash = crypto.createHash('sha256').update(data).digest('hex')7 return bigInt(hash,16)8}910function sha256hex(data) {11 return sha256(data).toString(16)12}1314exports.sha256 = sha25615exports.sha256hex = sha256hexwallets.js:
xxxxxxxxxx231const file = require('../file.js')2const changePage = require('../changepage').changePage3const blockchain = require('../blockchain.js')45function init() {6 document.getElementById('create').addEventListener('click',() => {7 changePage('wallets-create')8 })9 blockchain.calcBalances()10 file.getAll('wallets',(data) => {11 wallets = JSON.parse(data)12 var walletList = document.getElementById('wallet-list')13 var listItem14 wallets.forEach((wallet) => {15 listItem = document.createElement('div')16 listItem.classList.add('list-item')17 listItem.innerHTML = '<p><b>Name:</b> '+wallet.name+'</p><p><b>Public:</b> '+wallet.public+'</p><p><b>Amount:</b> <span class="money">'+wallet.amount/1000000+'</span></p>'18 walletList.appendChild(listItem)19 })20 },'[]')21}2223exports.init = initwallets-create.js:
xxxxxxxxxx301const ecdsa = require('../ecdsa.js')2const changePage = require('../changepage').changePage3const file = require('../file.js')45function init() {6 ecdsa.createKeys((public, private, err) => {7 if (err) {8 console.error(err)9 changePage('wallets')10 } else {11 document.getElementById('public').innerText = public12 document.getElementById('private').innerText = private13 }14 })15 document.getElementById('create').addEventListener('click',() => {16 var name = document.getElementById('name').value17 console.log('Creating wallet: '+name)18 var data = {}19 data['name'] = name20 data['public'] = document.getElementById('public').textContent21 data['private'] = document.getElementById('private').textContent22 data['amount'] = 023 console.log(JSON.stringify(data))24 file.append('wallets',data,() => {25 changePage('wallets')26 })27 })28}2930exports.init = initoverview.js:
xxxxxxxxxx671const file = require('../file')2const blockchain = require('../blockchain.js')3const ecdsa = require('../ecdsa.js')45function init() {6 // since it runs when you start the program7 // might as well check all the files exist8 file.getAll('wallets',(data) => {9 if (data === null || data === '' || data === '[]') {10 ecdsa.createKeys((public, private, err) => {11 if(err) {12 console.error(err)13 alert(err)14 } else {15 var wallet = {16 "name": "My Wallet",17 "public": public,18 "private": private,19 "amount": 020 }21 file.storeAll('wallets',[wallet])22 }23 })24 }25 })26 file.getAll('txpool',(data) => {27 if (data === null || data === '') {28 file.storeAll('txpool',[])29 }30 })31 file.getAll('recenttx',(data) => {32 if (data === null || data === '') {33 file.storeAll('recenttx',[])34 }35 })36 file.getAll('network-settings',(data) => {37 if (data === null || data === '') {38 var defaults = {39 "advertise": "true",40 "target-connections": 541 }42 file.storeAll('network-settings',defaults)43 }44 })45 file.getAll('blockchain',(data) => {46 if (data === null || data === '' || data === '[]') {47 file.storeAll('blockchain',{})48 }49 })50 file.getAll('balances',(data) => {51 if (data === null || data === '' || data === '[]') {52 file.storeAll('blockchain',{})53 }54 })55 file.getAll('connections',(data) => {56 if (data === null || data === '') {57 file.storeAll('connections',[])58 }59 })60 file.getAll('recent-connections',(data) => {61 if (data === null || data === '') {62 file.storeAll('recent-connections',[])63 }64 })65}6667exports.init = initnetwork-settings.js:
xxxxxxxxxx531const network = require('../network.js')2const file = require('../file.js')34function init() {56 // setting the current target connections7 file.get('target-connections','network-settings',(target) => {8 document.getElementById('curr').textContent = target9 })1011 // ping an IP12 document.getElementById('send').addEventListener('click',() => {13 file.get('advertise','network-settings',(data) => {14 var msg = {15 "header": {16 "type": "pg"17 },18 "body": {19 "advertise": data20 }21 }22 network.sendMsg(msg,document.getElementById('sendto').value)23 document.getElementById('pg-save').classList -= 'hidden'24 })25 })2627 // saving the "target number of connections"28 document.getElementById('target-save').addEventListener('click',() => {29 var min = document.getElementById('target').value30 file.store('target-connections',min,'network-settings',() => {31 document.getElementById('curr').textContent = min32 document.getElementById('min-save').classList -= 'hidden'33 })34 })3536 // saving the advertise toggle37 document.getElementById('save').addEventListener('click',() => {38 var options = document.getElementById('advertise')39 file.store('advertise',options.value,'network-settings',() => {40 document.getElementById('ad-save').classList -= 'hidden'41 })42 })4344 // refreshing the cache45 document.getElementById('refresh').addEventListener('click',() => {46 file.storeAll('connections','[]')47 document.getElementById('connections').textContent = 048 network.connect(false)49 document.getElementById('re-save').classList -= 'hidden'50 })51}5253exports.init = initmine.js:
xxxxxxxxxx501const Worker = require('tiny-worker')2const blockchain = require('../blockchain.js')3const network = require('../network.js')4const file = require('../file.js')5const remote = require('electron').remote67function init() {8 var miner = null9 var button = document.getElementById('toggle')10 var clear = document.getElementById('clear')11 var pre = document.getElementById('console')1213 clear.addEventListener('click',() => {14 pre.innerHTML = ''15 })1617 button.addEventListener('click',() => {18 if (button.textContent == 'Start') {19 if (miner === null) {20 try {21 miner = new Worker('js/mining-script.js')22 miner.onmessage = (msg) => {23 if(typeof msg.data === 'string') {24 pre.innerHTML += msg.data+'<br>'25 } else {26 console.log(JSON.stringify(msg.data))27 blockchain.addBlock(msg.data)28 network.sendToAll(msg.data)29 }30 }31 // Workers can't get remote so we need to send them the path manually32 var path = remote.app.getPath('appData')+'/arbitra-client/'33 miner.postMessage(path)34 } catch(e) {35 pre.innerHTML = 'Problem starting mining script, sorry :/'36 }37 }38 button.textContent = 'Stop'39 } else {40 if (miner !== null) {41 miner.terminate()42 miner = null43 }44 pre.innerHTML += 'Mining stopped<br>'45 button.textContent = 'Start'46 }47 })48}4950exports.init = initmake.js:
xxxxxxxxxx1201const file = require('../file.js')2const parse = require('../parse.js')3const ecdsa = require('../ecdsa.js')4const network = require('../network.js')56function init() {7 addInput()8 var add = document.getElementById('addInput')9 var send = document.getElementById('send')10 add.addEventListener('click',addInput)11 send.addEventListener('click',sendTx)12}1314function addInput() {15 var inputGroup = document.createElement('div')16 inputGroup.classList.add('input-group')17 // add select18 // <select name="dropdown"></select>19 var select = document.createElement('select')20 select.name = 'dropdown'21 // add placeholder22 select.innerHTML = '<option value="" selected disabled>Choose a wallet</option>'23 // add actual dropdown items24 populateDropdown(select)25 // add br26 // <br>27 var br = document.createElement('br')28 // add number input29 // <input name="amount" type="number" placeholder="Amount to send">30 var number = document.createElement('input')31 number.type = 'number'32 number.placeholder = 'Amount to send'33 number.name = 'amount'34 // add them all to the page35 inputGroup.appendChild(select)36 inputGroup.appendChild(br)37 inputGroup.appendChild(number)38 document.getElementById('inputs').appendChild(inputGroup)39}4041function populateDropdown(select) {42 var option43 // get list of wallets44 file.getAll('wallets',(data) => {45 var wallets = JSON.parse(data)46 wallets.forEach((wallet) => {47 option = document.createElement('option')48 option.value = wallet.public49 option.text = wallet.amount/1000000+"au - "+wallet.name50 select.add(option)51 })52 })53}5455function sendTx() {56 var to = document.getElementById('to').value57 // this isn't an array for some reason58 // we can make it one using Array.from59 // https://stackoverflow.com/a/37941811/545341960 var groups = Array.from(document.getElementsByClassName('input-group'))61 var message = {62 "header": {63 "type": "tx"64 },65 "body": {66 "to": to,67 "from": []68 }69 }70 file.getAll('wallets',(data) => {71 var time = Date.now()72 message.body['time'] = time73 // converting wallets into a format74 // where you can enter the public key75 // and get the private key76 var convert = {}77 var wallets = JSON.parse(data)78 wallets.forEach((wallet) => {79 public = wallet.public80 private = wallet.private81 convert[public] = private82 })83 try {84 groups.forEach((group) => {85 var child = group.childNodes86 var wallet = child[0].value87 console.log(wallet)88 // 2 because of the br89 var amount = child[2].value90 console.log(amount)91 if (wallet && amount > 0) {92 // convert to microau93 amount *= 100000094 // the message that is signed95 var concat = amount+to+time96 var signature = ecdsa.signMsg(concat,convert[wallet],(signature) => {97 message.body.from.push({98 "wallet": wallet,99 "amount": amount,100 "signature": signature101 })102 })103 } else {104 throw 'no amount entered'105 }106 })107 // if it's invalid, it will throw an error and be caught by the try-catch108 console.log('Transaction: '+JSON.stringify(message))109 parse.transaction(message.body)110 network.sendToAll(message)111 file.append('txpool',message.body)112 file.append('recenttx',message.body)113 } catch(e) {114 document.getElementById('error').classList.remove('hidden')115 console.warn('Tx failed: '+e)116 }117 },'[]')118}119120exports.init = initapp-settings.js:
xxxxxxxxxx431const file = require('../file.js')2const version = require('../../package.json').version3const fs = require('fs')4const network = require('../network.js')5const dialog = require('electron').remote.dialog67function init() {8 document.getElementById('version').textContent = version910 document.getElementById('save').addEventListener('click',() => {11 file.getAll('wallets',(data) => {12 dialog.showSaveDialog({13 filters: [14 {name:'JSON',extensions:['json']},15 {name:'All files',extensions:['*']}16 ]17 },(file) => {18 fs.writeFile(file,data,(err) => {19 if (err) throw err20 })21 })22 })23 })2425 document.getElementById('clear').addEventListener('click',() => {26 file.storeAll('blockchain',{})27 file.storeAll('balances',{})28 file.storeAll('connections',[])29 file.storeAll('network-settings',{"advertise":"true","target-connections":5})30 file.storeAll('recent-connections',[])31 file.storeAll('txpool',[])32 file.storeAll('recenttx',[])33 file.storeAll('sent',[])34 file.storeAll('error-log',[])35 document.getElementById('ca-save').classList.remove('hidden')36 document.getElementById('connections').textContent = 037 document.getElementById('height').textContent = 038 console.warn('All files wiped')39 network.connect(false)40 })41}4243exports.init = init